diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 000000000..0261021ce --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,29 @@ +name: Java CI + +on: [workflow_dispatch, push, pull_request] + +permissions: read-all + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + cache: [maven] + distribution: [temurin] + java: [17, 21, 24, 25-ea] + os: [ubuntu-latest] + fail-fast: false + max-parallel: 4 + name: Test JDK ${{ matrix.java }}, ${{ matrix.os }} + + steps: + - uses: actions/checkout@v5 + - name: Set up JDK ${{ matrix.java }} ${{ matrix.distribution }} + uses: actions/setup-java@v5 + with: + java-version: ${{ matrix.java }} + distribution: ${{ matrix.distribution }} + cache: ${{ matrix.cache }} + - name: Test with Maven + run: ./mvnw test -B -V --no-transfer-progress -D"license.skip=true" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..1d481b2df --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,49 @@ +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + schedule: + - cron: '26 13 * * 4' + +jobs: + analyze: + name: Analyze + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'java-kotlin' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Setup Java + uses: actions/setup-java@v5 + with: + cache: maven + distribution: 'temurin' + java-version: 21 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + queries: +security-and-quality + + - name: Autobuild + uses: github/codeql-action/autobuild@v4 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/coveralls.yaml b/.github/workflows/coveralls.yaml new file mode 100644 index 000000000..09ab9b4a7 --- /dev/null +++ b/.github/workflows/coveralls.yaml @@ -0,0 +1,29 @@ +name: Coveralls + +on: [push, pull_request] + +permissions: read-all + +jobs: + build: + if: github.repository_owner == 'mybatis' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Set up JDK + uses: actions/setup-java@v5 + with: + cache: maven + distribution: temurin + java-version: 21 + - name: Report Coverage to Coveralls for Pull Requests + if: github.event_name == 'pull_request' + run: ./mvnw -B -V test jacoco:report coveralls:report -q -Dlicense.skip=true -DrepoToken=$GITHUB_TOKEN -DserviceName=github -DpullRequest=$PR_NUMBER --no-transfer-progress + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.number }} + - name: Report Coverage to Coveralls for General Push + if: github.event_name == 'push' + run: ./mvnw -B -V test jacoco:report coveralls:report -q -Dlicense.skip=true -DrepoToken=$GITHUB_TOKEN -DserviceName=github --no-transfer-progress + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/site.yaml b/.github/workflows/site.yaml new file mode 100644 index 000000000..17572ee8e --- /dev/null +++ b/.github/workflows/site.yaml @@ -0,0 +1,32 @@ +name: Site + +on: + push: + branches: + - site + +permissions: + contents: write + +jobs: + build: + if: github.repository_owner == 'mybatis' && ! contains(toJSON(github.event.head_commit.message), '[maven-release-plugin]') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Set up JDK + uses: actions/setup-java@v5 + with: + cache: maven + distribution: temurin + java-version: 21 + - name: Build site + run: ./mvnw site site:stage -DskipTests -Dlicense.skip=true -B -V --no-transfer-progress --settings ./.mvn/settings.xml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NVD_API_KEY: ${{ secrets.NVD_API_KEY }} + - name: Deploy Site to gh-pages + uses: JamesIves/github-pages-deploy-action@v4 + with: + branch: gh-pages + folder: target/staging diff --git a/.github/workflows/sonar.yaml b/.github/workflows/sonar.yaml new file mode 100644 index 000000000..a43dd7198 --- /dev/null +++ b/.github/workflows/sonar.yaml @@ -0,0 +1,29 @@ +name: SonarCloud + +on: + push: + branches: + - master + +permissions: read-all + +jobs: + build: + if: github.repository_owner == 'mybatis' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + # Disabling shallow clone is recommended for improving relevancy of reporting + fetch-depth: 0 + - name: Set up JDK + uses: actions/setup-java@v5 + with: + cache: maven + distribution: temurin + java-version: 21 + - name: Analyze with SonarCloud + run: ./mvnw verify jacoco:report sonar:sonar -B -V -Dsonar.projectKey=mybatis_mybatis-dynamic-sql -Dsonar.organization=mybatis -Dsonar.host.url=https://sonarcloud.io -Dsonar.token=$SONAR_TOKEN -Dlicense.skip=true --no-transfer-progress + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/sonatype.yaml b/.github/workflows/sonatype.yaml new file mode 100644 index 000000000..9f47d2039 --- /dev/null +++ b/.github/workflows/sonatype.yaml @@ -0,0 +1,26 @@ +name: Sonatype + +on: + push: + branches: + - master + +permissions: read-all + +jobs: + build: + if: github.repository_owner == 'mybatis' && ! contains(toJSON(github.event.head_commit.message), '[maven-release-plugin]') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Set up JDK + uses: actions/setup-java@v5 + with: + cache: maven + distribution: temurin + java-version: 21 + - name: Deploy to Sonatype + run: ./mvnw deploy -DskipTests -B -V --no-transfer-progress --settings ./.mvn/settings.xml -Dlicense.skip=true + env: + CI_DEPLOY_USERNAME: ${{ secrets.CI_DEPLOY_USERNAME }} + CI_DEPLOY_PASSWORD: ${{ secrets.CI_DEPLOY_PASSWORD }} diff --git a/.gitignore b/.gitignore index 5e6b216e1..4d0ee68db 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ buildNumber.properties .DS_Store .idea *.iml +.github/keys/ diff --git a/travis/settings.xml b/.mvn/extensions.xml similarity index 52% rename from travis/settings.xml rename to .mvn/extensions.xml index 22861a8cb..fa0b7e819 100644 --- a/travis/settings.xml +++ b/.mvn/extensions.xml @@ -1,13 +1,13 @@ - - - - ossrh - ${env.CI_DEPLOY_USERNAME} - ${env.CI_DEPLOY_PASSWORD} - - - + + + fr.jcgay.maven + maven-profiler + 3.3 + + diff --git a/.mvn/maven.config b/.mvn/maven.config new file mode 100644 index 000000000..afdcfab79 --- /dev/null +++ b/.mvn/maven.config @@ -0,0 +1,2 @@ +-Daether.checksums.algorithms=SHA-512,SHA-256,SHA-1,MD5 +-Daether.connector.smartChecksums=false diff --git a/.mvn/settings.xml b/.mvn/settings.xml new file mode 100644 index 000000000..28f1a37e5 --- /dev/null +++ b/.mvn/settings.xml @@ -0,0 +1,52 @@ + + + + + + + + central + ${env.CI_DEPLOY_USERNAME} + ${env.CI_DEPLOY_PASSWORD} + + + + + gh-pages-scm + + branch + gh-pages + + + + + + github + ${env.GITHUB_TOKEN} + + + + + nvd + ${env.NVD_API_KEY} + + + + diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java index fa4f7b499..7e9c1e3c9 100644 --- a/.mvn/wrapper/MavenWrapperDownloader.java +++ b/.mvn/wrapper/MavenWrapperDownloader.java @@ -1,110 +1,96 @@ /* -Licensed to the Apache Software Foundation (ASF) under one -or more contributor license agreements. See the NOTICE file -distributed with this work for additional information -regarding copyright ownership. The ASF licenses this file -to you 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 + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ - http://www.apache.org/licenses/LICENSE-2.0 +import java.io.IOException; +import java.io.InputStream; +import java.net.Authenticator; +import java.net.PasswordAuthentication; +import java.net.URI; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.concurrent.ThreadLocalRandom; -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. -*/ +public final class MavenWrapperDownloader { + private static final String WRAPPER_VERSION = "3.3.4"; -import java.net.*; -import java.io.*; -import java.nio.channels.*; -import java.util.Properties; + private static final boolean VERBOSE = Boolean.parseBoolean(System.getenv("MVNW_VERBOSE")); -public class MavenWrapperDownloader { + public static void main(String[] args) { + log("Apache Maven Wrapper Downloader " + WRAPPER_VERSION); - /** - * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. - */ - private static final String DEFAULT_DOWNLOAD_URL = - "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"; - - /** - * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to - * use instead of the default one. - */ - private static final String MAVEN_WRAPPER_PROPERTIES_PATH = - ".mvn/wrapper/maven-wrapper.properties"; - - /** - * Path where the maven-wrapper.jar will be saved to. - */ - private static final String MAVEN_WRAPPER_JAR_PATH = - ".mvn/wrapper/maven-wrapper.jar"; - - /** - * Name of the property which should be used to override the default download url for the wrapper. - */ - private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; - - public static void main(String args[]) { - System.out.println("- Downloader started"); - File baseDirectory = new File(args[0]); - System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + if (args.length != 2) { + System.err.println(" - ERROR wrapperUrl or wrapperJarPath parameter missing"); + System.exit(1); + } - // If the maven-wrapper.properties exists, read it and check if it contains a custom - // wrapperUrl parameter. - File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); - String url = DEFAULT_DOWNLOAD_URL; - if(mavenWrapperPropertyFile.exists()) { - FileInputStream mavenWrapperPropertyFileInputStream = null; - try { - mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); - Properties mavenWrapperProperties = new Properties(); - mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); - url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); - } catch (IOException e) { - System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); - } finally { - try { - if(mavenWrapperPropertyFileInputStream != null) { - mavenWrapperPropertyFileInputStream.close(); - } - } catch (IOException e) { - // Ignore ... - } + try { + log(" - Downloader started"); + final URL wrapperUrl = URI.create(args[0]).toURL(); + final Path baseDir = Paths.get(".").toAbsolutePath().normalize(); + final Path wrapperJarPath = baseDir.resolve(args[1]).normalize(); + if (!wrapperJarPath.startsWith(baseDir)) { + throw new IOException("Invalid path: outside of allowed directory"); } + downloadFileFromURL(wrapperUrl, wrapperJarPath); + log("Done"); + } catch (IOException e) { + System.err.println("- Error downloading: " + e.getMessage()); + if (VERBOSE) { + e.printStackTrace(); + } + System.exit(1); } - System.out.println("- Downloading from: : " + url); + } - File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); - if(!outputFile.getParentFile().exists()) { - if(!outputFile.getParentFile().mkdirs()) { - System.out.println( - "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); - } + private static void downloadFileFromURL(URL wrapperUrl, Path wrapperJarPath) + throws IOException { + log(" - Downloading to: " + wrapperJarPath); + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + final String username = System.getenv("MVNW_USERNAME"); + final char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); } - System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); - try { - downloadFileFromURL(url, outputFile); - System.out.println("Done"); - System.exit(0); - } catch (Throwable e) { - System.out.println("- Error downloading"); - e.printStackTrace(); - System.exit(1); + Path temp = wrapperJarPath + .getParent() + .resolve(wrapperJarPath.getFileName() + "." + + Long.toUnsignedString(ThreadLocalRandom.current().nextLong()) + ".tmp"); + try (InputStream inStream = wrapperUrl.openStream()) { + Files.copy(inStream, temp, StandardCopyOption.REPLACE_EXISTING); + Files.move(temp, wrapperJarPath, StandardCopyOption.REPLACE_EXISTING); + } finally { + Files.deleteIfExists(temp); } + log(" - Downloader complete"); } - private static void downloadFileFromURL(String urlString, File destination) throws Exception { - URL website = new URL(urlString); - ReadableByteChannel rbc; - rbc = Channels.newChannel(website.openStream()); - FileOutputStream fos = new FileOutputStream(destination); - fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); - fos.close(); - rbc.close(); + private static void log(String msg) { + if (VERBOSE) { + System.out.println(msg); + } } } diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index af632155e..7bb288288 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,3 +1,4 @@ -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.0/apache-maven-3.6.0-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar - +wrapperVersion=3.3.4 +distributionType=source +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 52567dedb..000000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: java - -jdk: - - openjdk11 - - openjdk8 - -after_success: - - chmod -R 777 ./travis/after_success.sh - - ./travis/after_success.sh - -env: - global: - secure: b6V2NZgMyTmsTBo27a8YdEY3ZdPBY6G+ZwEIEJP9/xUABpG967MhXarIGvryIWco4WTnYVQeubstQQHP6zkGRccWpYDRmXRng9e8MvzATzHS67Z1YTVX+I5PQIxBSoKi6QgJMSnvCVU2REhsSfVqHYguYZ5ZBkIy9QidBmu2VElE36aFKOzFG96jLCMLaniM3ASu+Mcf58M5QRVNEQf5qTHAGubYY0k5Joib9oxlph4q+8zfvozD5XSlwDe9Hxd8Vls+jvCgWBPvj7ydf8qjQHfmaO+KdIa/yjS+a5Y5ILnLvNFPRIAkZSwTuLhaoNCGeLCjydCsL+eN9wpys568FlKz3cJvhDocVypXMXRXxFY/UnNZEoByxdEhB099cgbmHaGwGDXjC5xT9xgNsW5f+VkO1yW5WbiQWXH4vrbbvI/tuhWA3NBSc9bMTGXxRvIR4t30CDYGqIh1Rp9h2oz8lMtlg01GsXpRmPE7dX4Ec97vSoh5T4ddn7CIb6s98UgdeR3XA2uuGSgdyP3qUGh9jqdQJO0BLvql+QHqZQylW9KTjkxzr4py+90P4660weG0swGHxUeoZYQnJPU7h1VSydXIFdl9iGBcsTy8faeG5bBdBjZzYqBkgML6/yjDNvw/4VT1K+aWaJGK0QVa5GrQ1x4KIWRTTxpVep4zcS425RA= - -addons: - sonarcloud: - organization: "mybatis" - token: - secure: "Hci6OzYOkzcy6CUWlJTe5zLHRFRUO0KVe5o/V3RvtWndz3/4CHmWkP/dZjMxYliUiOZ+UR9uQhVH6dqcKSR94vb++CiDDxorkzqWiWlMEZMaWflvOe/dQsvKwPGZ/QO32RR+Boh0b3VrWZIOsu84wpYAMyZmncXJ0Y2QvmodiqtChYvV+8F6ZimNCfkGZRJhRfKnAbqFsPlB/c9TQc6hW0Dg9rWtjsbQFVtnkIhu48NdVtylPidHUSQWj0JsVQRvWUmBkoMr7lFSImyCW2r7X5vrVIccH6LTPk1Q5V4A8UgCtHhSnQcAFmFi/cxpUqIRgfjvB3azjp/Z9PrzZUFLgRzo3piKZkuQpI5a9xAANH7xcfiZfqztapLx6glW1c5oHRvk771dLcm5+he6JxKIKBBt8BFGOpkXG5NyvDOGHA4bo36oz6VL2L7oVRrS0KdgfFBmQFCuB0cRH3sQGGE4TjyghJUWLnYDRvQGz1IiX94xPUf/+MOHrMeLs/QIROOKJRTMyNC9u+ZQJeQK+i20b4A7euFP8fwzvQRJm3EgEuQf501raPUUDv+Kh4YskDg86fDHxJrmrpM4UsFe6V/WDPFGZc1zRJJRI5yeiaVg5M0KAt5SYfrV1YcjU5ExvKj7x9vyQ3pcfpLlKIKyFKBJMRryMQLNCn+frEJbxlrxJDE=" diff --git a/CHANGELOG.md b/CHANGELOG.md index c1bb7a6bb..9375a78e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,581 @@ This log will detail notable changes to MyBatis Dynamic SQL. Full details are available on the GitHub milestone pages. +## Release 2.0.0 - Unreleased + +Release 2.0.0 is a significant milestone for the library. We have moved to Java 17 as the minimum version supported. If +you are unable to move to this version of Java then the releases in the 1.x line can be used with Java 8. + +In addition, we have taken the opportunity to make changes to the library that may break existing code. We have +worked to make these changes as minimal as possible. + +### Potentially Breaking Changes: + +- If you use this library with MyBatis' Spring Batch integration, you will need to make changes as we have + refactored that support to be more flexible. Please see the + [Spring Batch](https://mybatis.org/mybatis-dynamic-sql/docs/springBatch.html) documentation page to see the new usage + details. +- If you have created any custom implementations of `SortSpecification`, you will need to update those + implementations due to a new rendering strategy for ORDER BY phrases. The old methods `isDescending` and `orderByName` + are removed in favor of a new method `renderForOrderBy` +- If you have implemented any custom functions, you will likely need to make changes. The supplied base classes now + hold an instance of `BasicColumn` rather than `BindableColumn`. This change was made to make the functions more + useful in variety of circumstances. If you follow the patterns shown on the + [Extending the Library](https://mybatis.org/mybatis-dynamic-sql/docs/extending.html) page, the change should be + limited to changing the private constructor to accept `BasicColumn` rather than `BindableColumn`. + +### Adoption of JSpecify (https://jspecify.dev/) + +Following the lead of many other projects (including The Spring Framework), we have adopted JSpecify to fully +document the null handling properties of this library. JSpecify is now a runtime dependency - as is +recommended practice with JSpecify. + +This change should not impact the running of any existing code, but depending on your usage you may see new IDE or +tooling warnings based on the declared nullability of methods in the library. You may choose to ignore the +warnings and things should continue to function. Of course, we recommend that you do not ignore these warnings! + +In general, the library does not expect that you will pass a null value into any method. There are two exceptions to +this rule: + +1. Some builder methods will accept a null value if the target object will properly handle null values through the + use of java.util.Optional +2. Methods with names that include "WhenPresent" will properly handle null parameters + (for example, "isEqualToWhenPresent") + +As you might expect, standardizing null handling revealed some issues in the library that may impact you. + +Fixing compiler warnings and errors: + +1. We expect that most of the warnings you encounter will be related to passing null values into a where condition. + These warnings should be resolved by changing your code to use the "WhenPresent" versions of methods as those + methods handle null values in a predictable way. +2. Java Classes that extend "AliasableSqlTable" will likely see IDE warnings about non-null type arguments. This can be + resolved by adding a "@NullMarked" annotation to the class or package. This issue does not affect Kotlin classes + that extend "AliasableSqlTable". +3. Similarly, if you have coded any functions for use with your queries, you can resolve most IDE warnings by adding + the "@NullMarked" annotation. +4. If you have coded any Kotlin functions that operate on a generic Java class from the library, then you should + change the type parameter definition to specify a non-nullable type. For example... + + ```kotlin + import org.mybatis.dynamic.sql.SqlColumn + + fun foo(column: SqlColumn) { + } + ``` + + Should change to: + + ```kotlin + import org.mybatis.dynamic.sql.SqlColumn + + fun foo(column: SqlColumn) { + } + ``` + +Runtime behavior changes: + +1. The where conditions (isEqualTo, isLessThan, etc.) can be filtered and result in an "empty" condition - + similar to java.util.Optional. Previously, calling a "value" method of the condition would return null. Now + those methods will throw "NoSuchElementException". This should not impact you in normal usage. +2. We have updated the "ParameterTypeConverter" used in Spring applications to maintain compatibility with Spring's + "Converter" interface. The primary change is that the framework will no longer call a type converter if the + input value is null. This should simplify the coding of converters and foster reuse with existing Spring converters. +3. The "map" method on the "WhenPresent" conditions will accept a mapper function that may return a null value. The + conditions will now properly handle this outcome + +### Other important changes: + +- The library now requires Java 17 +- Deprecated code from prior releases is removed +- We now allow CASE expressions in ORDER BY Clauses +- The "In" conditions will now throw `InvalidSqlException` during rendering if the list of values is empty. Previously + an empty In condition would render as invalid SQL and would usually cause a runtime exception from the database. + With this change, the exception thrown is more predictable and the error is caught before sending the SQL to the + database. +- All the paging methods (limit, offset, fetchFirst) now have "WhenPresent" variations that will drop the phrase from + rendering if a null value is passed in +- The JOIN syntax is updated and now allows full boolean expressions like a WHERE clause. The prior JOIN syntax + is deprecated and will be removed in a future release. +- Add support for locking options in select statements (for update, for share, etc.) This is not an abstraction of + these concepts for different databases it simply adds known clauses to a generated SQL statement. You should always + test to make sure these functions work in your target database. Currently, we support, and test, the options + supported by PostgreSQL. +- Rendering for all the conditions (isEqualTo, etc.) has changed. This should be transparent to most users unless you + have coded a direct implementation of `VisitableCondition`. The change makes it easier to code custom conditions that + are not supported by the library out of the box. The statement renderers now call methods `renderCondition` and + `renderLeftColumn` that you can override to implement any rendering you need. In addition, we've made `filter` and + `map` support optional if you implement custom conditions +- Added support for configuring a Java property name to be associated with an `SqlColumn`. This property name can be + used with the record based insert methods to reduce the boilerplate code for mapping columns to Java properties. + +## Release 1.5.2 - June 3, 2024 + +This is a small maintenance release with the following changes: + +1. Improvements to the Kotlin DSL for CASE expressions (infix methods for "else" and "then"). See this PR for + details: ([#785](https://github.com/mybatis/mybatis-dynamic-sql/pull/785)) +2. **Potentially Breaking Change**: the "in" conditions ("isIn", "isNotIn", "isInCaseInsensitive", + "isNotInCaseInsensitive") will now render if the input list of values is empty. This will lead + to a runtime exception. This change was made out of an abundance of caution and is the safest choice. + If you wish to allow "in" conditions to be removed from where clauses when the list is empty, + then use the "when present" versions of those conditions. If you are unsure how this works, please + read the documentation here: https://mybatis.org/mybatis-dynamic-sql/docs/conditions.html#optionality-with-the-%E2%80%9Cin%E2%80%9D-conditions + For background on the reason for the change, see the discussion here: https://github.com/mybatis/mybatis-dynamic-sql/issues/788 + +GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/milestone/14?closed=1](https://github.com/mybatis/mybatis-dynamic-sql/milestone/14?closed=1) + +**Important:** This is the last release that will be compatible with Java 8. + +## Release 1.5.1 - April 30, 2024 + +This is a minor release with several enhancements. + +GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/milestone/13?closed=1](https://github.com/mybatis/mybatis-dynamic-sql/milestone/13?closed=1) + +### Case Expressions and Cast Function +We've added support for CASE expressions to the library. Both simple and searched case expressions are supported. +This is a fairly extensive enhancement as case expressions are quite complex, but we were able to reuse many of the +building blocks from the WHERE and HAVING support already in the library. You should be able to build CASE expressions +with relatively few limitations. + +It is also common to use a CAST function with CASE expressions, so we have added CAST as a built-in function +in the library. + +The DSL for both Java and Kotlin has been updated to fully support CASE expressions in the same idiomatic forms +as other parts of the library. + +We've tested this extensively and the code is, of course, 100% covered by test code. But it is possible that we've not +covered every scenario. Please let us know if you find issues. + +Full documentation is available here: +- [Java Case Expression DSL Documentation](https://mybatis.org/mybatis-dynamic-sql/docs/caseExpressions.html) +- [Kotlin Case Expression DSL Documentation](https://mybatis.org/mybatis-dynamic-sql/docs/kotlinCaseExpressions.html) + +The pull request for this change is ([#761](https://github.com/mybatis/mybatis-dynamic-sql/pull/761)) + +### Parameter Values in Joins + +We've added the ability to specify typed values in equi-joins. This allows you to avoid the use of constants, and it is +type safe. For example: + +```java +SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(itemMaster, "im") + .join(orderLine, "ol").on(orderLine.itemId, equalTo(itemMaster.itemId)) + .and(orderLine.orderId, equalTo(1)) + .build() + .render(RenderingStrategies.MYBATIS3); +``` + +Note the phrase `and(orderLine.orderId, equalTo(1))` which will be rendered with a bound SQL parameter. Currently, this +capability is limited to equality only. If you have a use for other functions (not equal, less then, greater than, etc.) +please let us know. + +In order to add this capability, we've modified the join DSL to add type information to the join columns. This should +be source code compatible with most uses. There could be an issue if you are joining tables with columns of different +types - which is a rare usage. Please let us know if this causes an undo hardship. + +### Other Changes + +1. Rendering of conditions and columns was refactored. One benefit of this change is that + it is now easier to support more complex functions - such as the aggregate function `sum(id < 5)` which is the + initial enhancement request that inspired this change. As a result of the changes, one method is deprecated + in the `BasicColumn` object. If you have implemented any custom functions, please note this deprecation and update + your code accordingly. ([#662](https://github.com/mybatis/mybatis-dynamic-sql/pull/662)) +2. Added the ability to code a bound value in rendered SQL. This is similar to a constant, but the value is added to + the parameter map and a bind parameter marker is rendered. ([#738](https://github.com/mybatis/mybatis-dynamic-sql/pull/738)) +3. Refactored the conditions to separate the concept of an empty condition from that of a renderable condition. This + will enable a future change where conditions could decide to allow rendering even if they are considered empty (such + as rendering empty lists). This change should be transparent to users unless they have implemented custom conditions. +4. Added a configuration setting to allow empty list conditions to render. This could generate invalid SQL, but might be + a good safety measure in some cases. +5. Added Array based functions for the "in" and "not in" conditions in the Kotlin DSL. These functions allow a more + natural use of an Array as an input for an "in" condition. They also allow easy reuse of a vararg argument in a + function. ([#781](https://github.com/mybatis/mybatis-dynamic-sql/pull/781)) + +## Release 1.5.0 - April 21, 2023 + +GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/milestone/12?closed=1](https://github.com/mybatis/mybatis-dynamic-sql/milestone/12?closed=1) + +### Potentially Breaking Changes + +This release includes a major refactoring of the "where" clause support. This is done to support common code for +"having" clauses which is a new feature (see below). Most changes are source code compatible with previous +releases and should be transparent with no impact. Following is a list of some more visible changes... + +First, the "where" methods in `SqlBuilder` now return an instance of `WhereDSL.StandaloneWhereFinisher` rather than +`WhereDSL`. This will only impact you if you are using the WhereDSL directly which is a rare use case. + +Second, if you are using independent or reusable where clauses you will need to make changes. Previously you might have +coded an independent where clause like this: + +```java +private WhereApplier commonWhere = d -> d.where(id, isEqualTo(1)).or(occupation, isNull()); +``` + +Code like this will no longer compile. There are two options for updates. The simplest change to make is to +replace "where" with "and" or "or" in the above code. For example... + +```java +private WhereApplier commonWhere = d -> d.and(id, isEqualTo(1)).or(occupation, isNull()); +``` + +This will function as before, but you may think it looks a bit strange because the phrase starts with "and". If you +want this to look more like true SQL, you can write code like this: + +```java +private final WhereApplier commonWhere = where(id, isEqualTo(1)).or(occupation, isNull()).toWhereApplier(); +``` + +This uses a `where` method from the `SqlBuilder` class. + +### "Having" Clause Support + +This release adds support for "having" clauses in select statements. This includes a refactoring of the "where" +support, so we can reuse the and/or logic and rendering that is already present in the "where" clause support. +This because "having" and "where" are essentially the same. + +One slight behavior change with this refactoring is that the renderer will now remove a useless open/close +parentheses around certain rendered where clauses. Previously it was possible to have a rendered where clause like +this: + +```sql +where (a < 2 and b > 3) +``` + +The renderer will now remove the open/close parentheses in a case like this. + +In the Java DSL, a "having" clause can only be coded after a "group by" clause - which is a reasonable restriction +as "having" is only needed if there is a "group by". + +In the Kotlin DSL, the "group by" restriction is not present because of the free form nature of that DSL - but you +should probably only use "having" if there is a "group by". Also note that the freestanding "and" and "or" +functions in the Kotlin DSL still only apply to the where clause. For this reason, the freestanding "and" and "or" +methods are deprecated. Please only use the "and" and "or" methods inside a "where" or "having" lambda. + +The pull request for this change is ([#550](https://github.com/mybatis/mybatis-dynamic-sql/pull/550)) + +### Multi-Select Queries + +A multi-select query is a special case of a union select statement. The difference is that it allows "order by" and +paging clauses to be applied to the nested queries. A multi-select query looks like this: + +```java +SelectStatementProvider selectStatement = multiSelect( + select(id.as("A_ID"), firstName, lastName, birthDate, employed, occupation, addressId) + .from(person) + .where(id, isLessThanOrEqualTo(2)) + .orderBy(id) + .limit(1) +).unionAll( + select(id.as("A_ID"), firstName, lastName, birthDate, employed, occupation, addressId) + .from(person) + .where(id, isGreaterThanOrEqualTo(4)) + .orderBy(id.descending()) + .limit(1) +).orderBy(sortColumn("A_ID")) +.fetchFirst(2).rowsOnly() +.build() +.render(RenderingStrategies.MYBATIS3); +``` + +Notice how both inner queries have `order by` and `limit` phrases, then there is an `order by` phrase +for the entire query. + +The pull request for this change is ([#591](https://github.com/mybatis/mybatis-dynamic-sql/pull/591)) + +### Other Changes + +1. Added support for specifying "limit" and "order by" on the DELETE and UPDATE statements. Not all databases support + this SQL extension, and different databases have different levels of support. For example, MySQL/MariaDB have full + support but HSQLDB only supports limit as an extension to the WHERE clause. If you choose to use this new capability, + please test to make sure it is supported in your database. ([#544](https://github.com/mybatis/mybatis-dynamic-sql/pull/544)) +2. Deprecated Kotlin DSL functions have been removed, as well as deprecated support for "EmptyListCallback" in the "in" + conditions. ([#548](https://github.com/mybatis/mybatis-dynamic-sql/pull/548)) +3. Refactored the common insert mapper support for MyBatis3 by adding a CommonGeneralInsertMapper that can be used + without a class that matches the table row. It includes methods for general insert and insert select. + ([#570](https://github.com/mybatis/mybatis-dynamic-sql/pull/570)) +4. Added the ability to change a table name on AliasableSqlTable - this creates a new instance of the object with a new + name. This is useful in sharded databases where the name of the table is calculated based on some sharding + algorithm. Also deprecated the constructors on SqlTable that accept Suppliers for table name - this creates an + effectively mutable object and goes against the principles of immutability that we strive for in the library. + ([#572](https://github.com/mybatis/mybatis-dynamic-sql/pull/572)) +5. Add `SqlBuilder.concat` and the equivalent in Kotlin. This is a concatenate function that works on more databases. + ([#573](https://github.com/mybatis/mybatis-dynamic-sql/pull/573)) +6. Several classes and methods in the Kotlin DSL are deprecated in response to the new "having" support +7. Added support for inserting a list of simple classes like Integers, Strings, etc. This is via a new "map to row" + function on the insert, batch insert, and multirow insert statements. ([#612](https://github.com/mybatis/mybatis-dynamic-sql/pull/612)) + +## Release 1.4.1 - October 7, 2022 + +GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.4.1+](https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.4.1+) + +### Potentially Breaking Change + +In this release we have changed the default behavior of the library in one key area. If a where clause is coded, +but fails to render because all the optional conditionals drop out of the where clause, then the library will now +throw a `NonRenderingWhereClauseException`. We have made this change out of an abundance of caution. The prior +behavior would allow generation of statements that inadvertently affected all rows in a table. + +We have also deprecated the "empty callback" functions in the "in" conditions in favor of this new configuration +strategy. The "empty callback" methods were effective for "in" conditions that failed to render, but they offered +no help for other conditions that failed to render, or if all conditions fail to render - which is arguably a more +dangerous outcome. If you were using any of these methods, you should remove the calls to those methods and catch the +new `NonRenderingWhereClauseException`. + +If you desire the prior behavior where non rendering where clauses are allowed, you can change the global configuration +of the library or - even better - change the configuration of individual statements where this behavior should be allowed. + +For examples of global and statement configuration, see the "Configuration of the Library" page. + +### Other Changes + +1. Added support for criteria groups without an initial criteria. This makes it possible to create an independent list + of pre-created criteria and then add the list to a where clause. See the tests in the related pull request for + usage examples. ([#462](https://github.com/mybatis/mybatis-dynamic-sql/pull/462)) +2. Added the ability to specify a table alias on DELETE and UPDATE statements. + This is especially useful when working with a sub-query with an exists or not exists condition. + ([#489](https://github.com/mybatis/mybatis-dynamic-sql/pull/489)) +3. Updated the Kotlin DSL to use Kotlin 1.7's new "definitely non-null" types where appropriate. This helps us to more + accurately represent the nullable/non-nullable expectations for API method calls. + ([#496](https://github.com/mybatis/mybatis-dynamic-sql/pull/496)) +4. Added the ability to configure the library and change some default behaviors. Currently, this is limited to changing + the behavior of the library in regard to where clauses that will not render. See the "Configuration of the Library" + page for details. ([#515](https://github.com/mybatis/mybatis-dynamic-sql/pull/515)) +5. Added several checks for invalid SQL ([#516](https://github.com/mybatis/mybatis-dynamic-sql/pull/516)) +6. Added documentation for the various exceptions thrown by the library ([#517](https://github.com/mybatis/mybatis-dynamic-sql/pull/517)) +7. Update the "insertSelect" method in the Kotlin DSL to make it consistent with the other insert methods ([#524](https://github.com/mybatis/mybatis-dynamic-sql/pull/524)) + +## Release 1.4.0 - March 3, 2022 + +The release includes new functionality in the Where Clause DSL to support arbitrary grouping of conditions, and also use +of a "not" condition. It should now be possible to write any type of where clause. + +Additionally, there were significant updates to the Kotlin DSL - both to support the new functionality in the +where clause, and significant updates to insert statements. There were also many minor updates in Kotlin +to make more use of Kotlin language features like infix functions and operator overloads. + +GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.4.0+](https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.4.0+) + +1. Added support for arbitrary placement of nested criteria. For example, it is now + possible to write a where clause like this: `where (a < 5 and B = 3) and ((C = 4 or D = 5) and E = 6)`. Previously + we did not support the grouping of criteria at the beginning of a where clause or the beginning of an and/or + condition. Adding this support required significant refactoring, but that should be transparent to most users. + ([#434](https://github.com/mybatis/mybatis-dynamic-sql/pull/434)) +2. Remove deprecated "when" and "then" methods on all conditions. The methods have been replaced by more appropriately + named "filter" and "map" methods that function as expected for method chaining. + ([#435](https://github.com/mybatis/mybatis-dynamic-sql/pull/435)) +3. Added support for a "not" criteria grouping on a where clause. It is now possible to write a where clause like + `where (a < 5 and B = 3) and (not (C = 4 or D = 5) and E = 6)`. With this enhancement (and the enhancement for + arbitrary grouping) it should now be possible to write virtually any where clause imaginable. + ([#438](https://github.com/mybatis/mybatis-dynamic-sql/pull/438)) +4. Major update to the Kotlin where clause DSL. Where clauses now support the "group" and "not" features from above. In + addition, the where clause DSL has been fully updated to make it feel more like natural SQL. The previous version + of the where clause DSL would have yielded almost unreadable code had the "group" and "not" functions been added. + This update is better all around and yields a DSL that is very similar to native SQL. The new DSL includes many + Kotlin DSL construction features including infix functions, operator overloads, and functions with receivers. + We believe it will be well worth the effort to migrate to the new DSL. The prior where clause DSL remains in the + library for now, but is deprecated. It will be removed in version 1.5.0 of the library. Documentation for the new + DSL is here: https://github.com/mybatis/mybatis-dynamic-sql/blob/master/src/site/markdown/docs/kotlinWhereClauses.md + ([#442](https://github.com/mybatis/mybatis-dynamic-sql/pull/442)) +5. General cleanup of the Kotlin DSL. The Kotlin DSL functions are now mostly Unit functions. This should have + no impact on most users and is source code compatible with prior versions of the library when the library was used + as described in the documentation. This change greatly simplifies the type hierarchy of the Kotlin builders. + ([#446](https://github.com/mybatis/mybatis-dynamic-sql/pull/446)) +6. Minor update the Kotlin join DSL to make it closer to natural SQL. The existing join methods are deprecated and + will be removed in version 1.5.0. ([#447](https://github.com/mybatis/mybatis-dynamic-sql/pull/447)) +7. Updated most of the Kotlin insert DSL functions to be more like natural SQL. The main difference is that for insert, + insertBatch, and insertMultiple, the "into" function is moved inside the completer lambda. The old methods are now + deprecated and will be removed in version 1.5.0 of the library. This also allowed us to make some insert DSL + methods into infix functions. ([#452](https://github.com/mybatis/mybatis-dynamic-sql/pull/452)) +8. Updated the where clause to expose table aliases specified in an outer query to sub queries in the where clause + (either an "exists" clause, or a sub query to column comparison condition) This makes it easier to use these types + of sub queries without having to re-specify the aliases for columns from the outer query. + ([#459](https://github.com/mybatis/mybatis-dynamic-sql/pull/459)) + +## Release 1.3.1 - December 18, 2021 + +This is a minor release with a few small enhancements. Most deprecated methods will be removed in the next release. + +GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.3.1+](https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.3.1+) + +### Added + +- Added the ability to specify a JavaType associated with a column. The JavaType will be rendered properly for MyBatis ([#386](https://github.com/mybatis/mybatis-dynamic-sql/pull/386)) +- Added a few missing groupBy and orderBy methods on the `select` statement ([#409](https://github.com/mybatis/mybatis-dynamic-sql/pull/409)) +- Added a check for when a table alias is re-used in error (typically in a self-join) ([#425](https://github.com/mybatis/mybatis-dynamic-sql/pull/425)) +- Added a new extension of SqlTable that supports setting a table alias directly within the table definition ([#426](https://github.com/mybatis/mybatis-dynamic-sql/pull/426)) + +## Release 1.3.0 - May 6, 2021 + +GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.3.0+](https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.3.0+) + +### Release Themes + +The major themes of this release include the following: + +1. Add support for subqueries in select statements - both in a from clause and a join clause. +1. Add support for the "exists" and "not exists" operator. This will work in "where" clauses anywhere + they are supported. +1. Refactor and improve the built-in conditions for consistency (see below). There is one breaking change also + detailed below. +1. Continue to refine the Kotlin DSL. Many changes to the Kotlin DSL are internal and should be source code + compatible with existing code. There is one breaking change detailed below. +1. Remove deprecated code from prior releases. + +### Built-In Condition Refactoring and Breaking Change +All built-in conditions have been refactored. The changes should have little impact for the vast majority of users. +However, there are some changes in behavior and one breaking change. + +1. Internally, the conditions no longer hold value Suppliers, they now hold the values themselves. The SqlBuilder + methods that accept Suppliers will call the `Supplier.get()` method when the condition is constructed. This should + have no impact unless you were somehow relying on the delay in obtaining a value until the condition was rendered. +1. The existing "then" and "when" methods have been deprecated and replaced with "map" and "filter" respectively. + The new method names are more familiar and more representative of what these methods actually do. In effect + these methods mimic the function of the "map" and "filter" methods on "java.util.Optional" and they are used + for a similar purpose. +1. The new "filter" method works a bit differently than the "when" method it replaces. The old "when" method could not + be chained - if it was called multiple times, only the last call would take effect. The new "filter" methods works + as it should and every call will take effect. This allows you to construct map/filter pipelines as you would + expect. +1. The new "map" method will allow you to change the datatype of a condition as is normal for a "map" method. You + can use this method to apply a type conversion directly within condition. +1. All the "WhenPresent" conditions have been removed as separate classes. The methods that produced these conditions + in the SqlBuilder remain, and they will now produce a condition with a "NotNull" filter applied. So at the API level + things will function exactly as before, but the intermediate classes will be different. +1. One **breaking change** is that the builder for List value conditions has been removed without replacement. If you + were using this builder to supply a "value stream transformer", then the replacement is to build a new List value + condition and then call the "map" and "filter" methods as needed. For example, prior code looked like this + + ```java + public static IsIn isIn(String...values) { + return new IsIn.Builder() + .withValues(Arrays.asList(values)) + .withValueStreamTransformer(s -> s.filter(Objects::nonNull) + .map(String::trim) + .filter(st -> !st.isEmpty())) + .build(); + } + ``` + New code should look like this: + ```java + public static IsIn isIn(String...values) { + return SqlBuilder.isIn(values) + .filter(Objects::nonNull) + .map(String::trim) + .filter(st -> !st.isEmpty()); + } + ``` + We think this is a marked improvement! + +### Kotlin DSL Update and Breaking Change for Kotlin + +The Kotlin DSL continues to evolve. With this release we have fully built out the DSL, and it is no longer necessary +to use any functions in `org.mybatis.dynamic.sql.SqlBuilder`. The advantages of this are many and are detailed on the +Kotlin overview page in the documentation. Many functions in `SqlBuilder` have been replaced by +top level functions the `org.mybatis.dynamic.sql.util.kotlin.elements` package. In most cases you can switch to the +native Kotlin DSL by simply changing the import statements. For example, you can switch usage of the `isEqualTo` +function by changing + +```kotlin +import org.mybatis.dynamic.sql.SqlBuilder.isEqualTo +``` + +to + +```kotlin +import org.mybatis.dynamic.sql.util.kotlin.elements.isEqualTo +``` + +Several functions that accepted supplier arguments are not present in the Kotlin DSL. This is to avoid difficult +and confusing method overload problems for methods that did not offer any real benefit. If you were using one of these +methods in the Java DSL, then in the Kotlin DSL you will have to change the function argument from a supplier to the +actual value itself. + +A **breaking change** is that Kotlin support for `select` and `count` statements has been refactored. This will not impact code +created by MyBatis generator. It will have an impact on Spring/Kotlin users as well as MyBatis users that coded joins or +other queries directly in Kotlin. The difference is that the `from` clause has been moved inside the lambda for select +and count statements. + +Previously, code looked like this: +```kotlin + val selectStatement = select(foo).from(bar) { + where(id, isLessThan(3)) + } +``` + +The new code looks like this: +```kotlin + val selectStatement = select(foo) { + from(bar) + where(id, isLessThan(3)) + } +``` + +This change makes the Kotlin DSL more consistent and also makes it easier to implement subquery support in the +Kotlin DSL. + +### Added + +- Added a new sort specification that is useful in selects with joins ([#269](https://github.com/mybatis/mybatis-dynamic-sql/pull/269)) +- Added the capability to generate a camel cased alias for a column ([#272](https://github.com/mybatis/mybatis-dynamic-sql/issues/272)) +- Added subquery support for "from" clauses in a select statement ([#282](https://github.com/mybatis/mybatis-dynamic-sql/pull/282)) +- Added Kotlin DSL updates to support sub-queries in select statements, where clauses, and insert statements ([#282](https://github.com/mybatis/mybatis-dynamic-sql/pull/282)) +- Added subquery support for "join" clauses in a select statement ([#293](https://github.com/mybatis/mybatis-dynamic-sql/pull/293)) +- Added support for the "exists" and "not exists" operator in where clauses ([#296](https://github.com/mybatis/mybatis-dynamic-sql/pull/296)) +- Refactored the built-in conditions ([#331](https://github.com/mybatis/mybatis-dynamic-sql/pull/331)) ([#336](https://github.com/mybatis/mybatis-dynamic-sql/pull/336)) +- Added composition functions for WhereApplier ([#335](https://github.com/mybatis/mybatis-dynamic-sql/pull/335)) +- Added a mapping for general insert and update statements that will render null values as "null" in the SQL ([#343](https://github.com/mybatis/mybatis-dynamic-sql/pull/343)) +- Allow the "in when present" conditions to accept a null Collection as a parameter ([#346](https://github.com/mybatis/mybatis-dynamic-sql/pull/346)) +- Add Better Support for MyBatis Multi-Row Inserts that Return Generated Keys ([#349](https://github.com/mybatis/mybatis-dynamic-sql/pull/349)) +- Major improvement to the Kotlin DSL ([#353](https://github.com/mybatis/mybatis-dynamic-sql/pull/353)) +- Remove use of "record" as an identifier (it is restricted in JDK16) ([#357](https://github.com/mybatis/mybatis-dynamic-sql/pull/357)) + +## Release 1.2.1 - September 29, 2020 + +GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.2.1+](https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.2.1+) + +### Fixed + +- Fixed a bug where the In conditions could render incorrectly in certain circumstances. ([#239](https://github.com/mybatis/mybatis-dynamic-sql/issues/239)) + +### Added + +- Added a callback capability to the "In" conditions that will be called before rendering when the conditions are empty. Also, removed the option that forced the library to render invalid SQL in that case. ([#241](https://github.com/mybatis/mybatis-dynamic-sql/pull/241)) +- Added a utility mapper for MyBatis that allows you to run any select query without having to predefine a result mapping. ([#255](https://github.com/mybatis/mybatis-dynamic-sql/pull/255)) +- Added utility mappers for MyBatis that allow you to run generic CRUD operations. ([#263](https://github.com/mybatis/mybatis-dynamic-sql/pull/263)) + +## Release 1.2.0 - August 19, 2020 + +GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.2.0+](https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.2.0+) + +### General Announcements + +This release includes major improvements to the Spring support in the library. Spring support is now functionally equivalent to MyBatis support. + +This release includes a significant refactoring of the classes in the "org.mybatis.dynamic.sql.select.function" package. The new classes are more consistent and flexible and should be compatible with existing code at the source level (meaning code should be recompiled for the new version of the library). If you have written your own set of functions to extend the library, you will notice that the base classes 'AbstractFunction" and "AbstractMultipleColumnArithmeticFunction" are now deprecated. Their replacement classes are "AbstractUniTypeFunction" and "OperatorFunction" respectively. + +With this release, we deprecated several insert methods because they were inconsistently named or awkward. All deprecated methods have documented direct replacements. + +All deprecated code will be removed in the next minor release. + +### Added + +- Added a general insert statement that does not require a separate record class to hold values for the insert. ([#201](https://github.com/mybatis/mybatis-dynamic-sql/issues/201)) +- Added the capability to specify a rendering strategy on a column to override the default rendering strategy for a statement. This will allow certain edge cases where a parameter marker needs to be formatted uniquely (for example, "::jsonb" needs to be added to parameter markers for JSON fields in PostgreSQL) ([#200](https://github.com/mybatis/mybatis-dynamic-sql/issues/200)) +- Added the ability to write a function that will change the column data type ([#197](https://github.com/mybatis/mybatis-dynamic-sql/issues/197)) +- Added the `applyOperator` function to make it easy to use non-standard database operators in expressions ([#220](https://github.com/mybatis/mybatis-dynamic-sql/issues/220)) +- Added convenience methods for count(column) and count(distinct column) ([#221](https://github.com/mybatis/mybatis-dynamic-sql/issues/221)) +- Added support for union queries in Kotlin ([#187](https://github.com/mybatis/mybatis-dynamic-sql/issues/187)) +- Added the ability to write "in" conditions that will render even if empty ([#228](https://github.com/mybatis/mybatis-dynamic-sql/issues/228)) +- Many enhancements for Spring including: + - Fixed a bug where multi-row insert statements did not render properly for Spring ([#224](https://github.com/mybatis/mybatis-dynamic-sql/issues/224)) + - Added support for a parameter type converter for use cases where the Java type of a column does not match the database column type ([#131](https://github.com/mybatis/mybatis-dynamic-sql/issues/131)) + - Added a utility class which simplifies the use of the named parameter JDBC template for Java code - `org.mybatis.dynamic.sql.util.spring.NamedParameterJdbcTemplateExtensions` + - Added support for general inserts, multi-row inserts, batch inserts in the Kotlin DSL for Spring ([#225](https://github.com/mybatis/mybatis-dynamic-sql/issues/225)) + - Added support for generated keys in the Kotlin DSL for Spring ([#226](https://github.com/mybatis/mybatis-dynamic-sql/issues/226)) + ## Release 1.1.4 - November 23, 2019 GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.1.4+](https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.1.4+) @@ -26,7 +601,6 @@ GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/issues?q=miles - Utility classes and a new canonical pattern for MyBatis Generator (CRUD) mappers ([#118](https://github.com/mybatis/mybatis-dynamic-sql/issues/118)) ([#125](https://github.com/mybatis/mybatis-dynamic-sql/pull/125)) ([#128](https://github.com/mybatis/mybatis-dynamic-sql/pull/128)) - Kotlin Extensions and Kotlin DSL ([#133](https://github.com/mybatis/mybatis-dynamic-sql/pull/133)) ([#139](https://github.com/mybatis/mybatis-dynamic-sql/pull/139)) - ## Release 1.1.2 - July 5, 2019 GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.1.2+](https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.1.2+) @@ -34,13 +608,12 @@ GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/issues?q=miles ### Added - Changed the public SQLBuilder API to accept Collection instead of List for in conditions and batch record inserts. This should have no impact on existing code, but allow for some future flexibility ([#88](https://github.com/mybatis/mybatis-dynamic-sql/pull/88)) -- Added the ability have have table catalog and/or schema calculated at query runtime. This is useful for situations where there are different database schemas for different environments, or in some sharding situations ([#92](https://github.com/mybatis/mybatis-dynamic-sql/pull/92)) +- Added the ability to have table catalog and/or schema calculated at runtime. This is useful for situations where there are different database schemas for different environments, or in some sharding situations ([#92](https://github.com/mybatis/mybatis-dynamic-sql/pull/92)) - Add support for paging queries with "offset" and "fetch first" - this seems to be standard on most databases ([#96](https://github.com/mybatis/mybatis-dynamic-sql/pull/96)) - Added the ability to call a builder method on any intermediate object in a select statement and receive a fully rendered statement. This makes it easier to build very dynamic queries ([#106](https://github.com/mybatis/mybatis-dynamic-sql/pull/106)) - Add the ability to modify values on any condition before they are placed in the parameter map ([#105](https://github.com/mybatis/mybatis-dynamic-sql/issues/105)) - Add the ability to call `where()` with no parameters. This aids in constructing very dynamic queries ([#107](https://github.com/mybatis/mybatis-dynamic-sql/issues/107)) - ## Release 1.1.1 - April 7, 2019 GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.1.1+](https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.1.1+) @@ -50,14 +623,13 @@ GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/issues?q=miles - Limit and offset support in the select statement - Utilities for Spring Batch - All conditions now support conditional rendering with lambdas -- Select * support +- Select \* support - Union all support ### Bugs Fixed - Fixed self joins - ## Release 1.1.0 - April 24, 2018 GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.1.0+](https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.1.0+) diff --git a/LICENSE b/LICENSE index 8dada3eda..7e835b2fa 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ Apache License Version 2.0, January 2004 - http://www.apache.org/licenses/ + https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -178,7 +178,7 @@ APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" + boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a @@ -186,16 +186,17 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright {yyyy} {name of copyright owner} + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + 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. + diff --git a/license.txt b/LICENSE_HEADER similarity index 92% rename from license.txt rename to LICENSE_HEADER index 4ce1777ad..a81590a5b 100644 --- a/license.txt +++ b/LICENSE_HEADER @@ -4,7 +4,7 @@ you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + 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, diff --git a/NOTICE b/NOTICE index fc3fc60c3..c7bfb5169 100644 --- a/NOTICE +++ b/NOTICE @@ -1,2 +1,8 @@ +MyBatis Dynamic Sql +Copyright 2016-2023 + +This product includes software developed by +The MyBatis Team (https://www.mybatis.org/). + For testing purposes only, this product includes the Animals2 dataset from https://vincentarelbundock.github.io/Rdatasets/datasets.html diff --git a/README.md b/README.md index 7e950bb13..3a536a47a 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,87 @@ # MyBatis Dynamic SQL -[![Build Status](https://travis-ci.org/mybatis/mybatis-dynamic-sql.svg?branch=master)](https://travis-ci.org/mybatis/mybatis-dynamic-sql) +[![Build Status](https://github.com/mybatis/mybatis-dynamic-sql/workflows/Java%20CI/badge.svg?branch=master)](https://github.com/mybatis/mybatis-dynamic-sql/actions?query=workflow%3A%22Java+CI%22) [![Coverage Status](https://coveralls.io/repos/github/mybatis/mybatis-dynamic-sql/badge.svg?branch=master)](https://coveralls.io/github/mybatis/mybatis-dynamic-sql?branch=master) [![Maven central](https://maven-badges.herokuapp.com/maven-central/org.mybatis.dynamic-sql/mybatis-dynamic-sql/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.mybatis.dynamic-sql/mybatis-dynamic-sql) [![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/https/oss.sonatype.org/org.mybatis.dynamic-sql/mybatis-dynamic-sql.svg)](https://oss.sonatype.org/content/repositories/snapshots/org/mybatis/dynamic-sql/mybatis-dynamic-sql/) -[![License](http://img.shields.io/:license-apache-brightgreen.svg)](http://www.apache.org/licenses/LICENSE-2.0.html) +[![License](https://img.shields.io/:license-apache-brightgreen.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=mybatis_mybatis-dynamic-sql&metric=alert_status)](https://sonarcloud.io/dashboard?id=mybatis_mybatis-dynamic-sql) [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=mybatis_mybatis-dynamic-sql&metric=security_rating)](https://sonarcloud.io/dashboard?id=mybatis_mybatis-dynamic-sql) ## What Is This? -This library is a framework for generating dynamic SQL statements. Think of it as a typesafe SQL templating library, -with additional support for MyBatis3 and Spring JDBC Templates. +This library is a general purpose SQL generator. Think of it as a typesafe and expressive SQL DSL (domain specific +language), with support for rendering SQL formatted properly for MyBatis3 and Spring's NamedParameterJDBCTemplate. -The library will generate full DELETE, INSERT, SELECT, and UPDATE statements formatted for use by MyBatis or Spring. -The most common use case is to generate statements, and a matching set of parameters, that can be directly used -by MyBatis. The library will also generate statements and parameter objects that are compatible with Spring JDBC -templates. +The library also contains extensions for Kotlin that enable an idiomatic Kotlin DSL for SQL. -The library works by implementing an SQL-like DSL that creates an object containing a full SQL statement and any -parameters required for that statement. The SQL statement object can be used directly by MyBatis as a parameter to a mapper method. +The library will generate full DELETE, INSERT, SELECT, and UPDATE statements. The DSL implemented by the +library is very similar to native SQL, but it includes many functions that allow for very dynamic SQL statements. +For example, a typical search can be coded with a query like this (the following code is Kotlin, but Java code is very +similar): -The library also contains extensions for Kotlin that enable an idiomatic Kotlin DSL. +```kotlin + data class SearchParameters(val id: String?, val firstName: String?, val lastName: String?) -See the following pages for further information: + fun search(searchParameters: SearchParameters) = + select(id, firstName, lastName) { + from(Customer) + where { + active isEqualTo true + and { id isEqualToWhenPresent searchParameters.id } + and { + firstName(isLikeCaseInsensitiveWhenPresent(searchParameters.firstName) + .map { "%" + it.trim() + "%" }) + } + and { + lastName(isLikeCaseInsensitiveWhenPresent(searchParameters.lastName) + .map { "%" + it.trim() + "%" }) + } + } + orderBy(lastName, firstName) + limit(500) + } +``` -| Page | Comments| -|------|---------| -|[Quick Start](src/site/markdown/docs/quickStart.md) | Shows a complete example of building code for this library | -|[MyBatis3 Support](src/site/markdown/docs/mybatis3.md) | Information about specialized support for [MyBatis3](https://github.com/mybatis/mybatis-3). The examples on this page are similar to the code generated by [MyBatis Generator](https://github.com/mybatis/generator) | -|[Kotlin Support with MyBatis3](src/site/markdown/docs/kotlinMyBatis3.md) | Information about the Kotlin extensions and Kotlin DSL when using MyBatis3 as the runtime | -|[Spring Support](src/site/markdown/docs/spring.md) | Information about specialized support for Spring JDBC Templates | -|[Kotlin Support with Spring](src/site/markdown/docs/kotlinSpring.md) | Information about the Kotlin extensions and Kotlin DSL when using Spring JDBC Template as the runtime | -|[Spring Batch Support](src/site/markdown/docs/springBatch.md) | Information about specialized support for Spring Batch using the [MyBatis Spring Integration](https://github.com/mybatis/spring) | +This query does quite a lot... -## Requirements +1. It is a search with three search criteria - any combination of search criteria can be used +2. Only records with an active status will be returned +3. If `id` is specified, it will be used as a filter +4. If `firstName` is specified, it will be used in a case-insensitive search and SQL wildcards will be appended +5. If `lastName` is specified, it will be used in a case-insensitive search and SQL wildcards will be appended +6. The query results are limited to 500 rows -The library has no dependencies. Java 8 or higher is required. +Using the dynamic SQL features of the library eliminates a lot of code that would be required for checking nulls, +adding wild cards, etc. This query clearly expresses the intent of the search in just a few lines. + +See the following pages for detailed information: + +| Page | Comments | +|--------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [Quick Start](src/site/markdown/docs/quickStart.md) | Shows a complete example of building code for this library | +| [MyBatis3 Support](src/site/markdown/docs/mybatis3.md) | Information about specialized support for [MyBatis3](https://github.com/mybatis/mybatis-3). The examples on this page are similar to the code generated by [MyBatis Generator](https://github.com/mybatis/generator) | +| [Kotlin Support with MyBatis3](src/site/markdown/docs/kotlinMyBatis3.md) | Information about the Kotlin extensions and Kotlin DSL when using MyBatis3 as the runtime | +| [Spring Support](src/site/markdown/docs/spring.md) | Information about specialized support for Spring JDBC Templates | +| [Kotlin Support with Spring](src/site/markdown/docs/kotlinSpring.md) | Information about the Kotlin extensions and Kotlin DSL when using Spring JDBC Template as the runtime | +| [Spring Batch Support](src/site/markdown/docs/springBatch.md) | Information about specialized support for Spring Batch using the [MyBatis Spring Integration](https://github.com/mybatis/spring) | + +The library test cases provide several complete examples of using the library in various different styles: + +| Language | Runtime | Comments | Code Directory | +|----------|------------------------------------------|------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------| +| Java | MyBatis3 | Example using Java utility classes for MyBatis in the style of MyBatis Generator | [../examples/simple](src/test/java/examples/simple) | +| Java | MyBatis3 + MyBatis-Spring | Example using MyBatis-Spring integration | [../examples/column/comparison](src/test/java/examples/column/comparison) | +| Java | MyBatis3 + MyBatis-Spring (Spring Batch) | Example using Java utility classes for the MyBatis integration with Spring Batch | [../examples/springbatch](src/test/java/examples/springbatch) | +| Java | Spring JDBC | Example using Java utility classes for Spring JDBC Template | [../examples/spring](src/test/java/examples/spring) | +| Kotlin | MyBatis3 | Example using Kotlin utility classes for MyBatis in the style of MyBatis Generator | [../examples/kotlin/mybatis3/canonical](src/test/kotlin/examples/kotlin/mybatis3/canonical) | +| Kotlin | MyBatis3 + MyBatis-Spring | Example using MyBatis-Spring integration in Kotlin | [../examples/kotlin/mybatis3/column/comparison](src/test/kotlin/examples/kotlin/mybatis3/column/comparison) | +| Kotlin | Spring JDBC | Example using Kotlin utility classes for Spring JDBC Template | [../examples/kotlin/spring/canonical](src/test/kotlin/examples/kotlin/spring/canonical) | + + +## Requirements and Dependencies + +Version 2.x requires Java 17 and has a required runtime dependency on JSpecify (https://jspecify.dev/). Version 1.x +requires Java 8 and has no required runtime dependencies. + +All versions have support for MyBatis3, Spring Framework, and Kotlin - all those dependencies are optional. The library +should work in those environments as the dependencies will be made available at runtime. diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 000000000..1077f07e2 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,54 @@ +# Release Information + +The project is released with the normal Maven release cycle except for the site. + +## Pre-Requisites + +1. Get authorization to Sonatype and make sure your GPG key is setup and registered per instructions here: + https://github.com/mybatis/committers-stuff/wiki/Release-Process +2. Make sure your SSH key is setup at GitHub + +## Preparation + +1. Update the release date in the CHANGELOG + +## Release Process + +1. Clone the main repo (with ssh), checkout the master branch +2. mvn release:prepare +3. mvn release:perform +4. Logon to https://oss.sonatype.org/ +5. Find the mybatis staging repo +6. Verify everything looks OK +7. Close the repo +8. Release the repo + +## Update the Site + +Automatic site deployment plugin is broken on Mac M1 and with newer keys on any platform. Therefore, it is disabled +in the normal release. Here's how to do it manually: + +1. Clone the main repo and checkout the release tag: + - `git clone git@github.com:mybatis/mybatis-dynamic-sql.git` + - `cd mybatis-dynamic-sql` + - `git checkout mybatis-dynamic-sql-1.5.1` +2. `./mvnw clean site` +3. Checkout a copy of the main repo in a temp directory: + - `mkdir ~/temp/temp-mybatis` + - `cd ~/temp/temp-mybatis` + - `git clone git@github.com:mybatis/mybatis-dynamic-sql.git` + - `cd mybatis-dynamic-sql` + - `git checkout gh-pages` +4. Copy the generated site into the temp checkout: + - `cp -R <>/mybatis-dynamic-sql/target/site/ ~/temp/temp-mybatis/mybatis-dynamic-sql` +5. Push the new site: + - `cd ~/temp/temp-mybatis/mybatis-dynamic-sql` + - `git add .` + - `git commit -m "Manual Site Update 1.5.1"` + - `git push` +6. Delete the temporary checkout + - `rm -R ~/temp/temp-mybatis` + +## After Releasing + +Draft a new release on GitHub and tie it to the new release tag. diff --git a/checkstyle-override.xml b/checkstyle-override.xml index 2d62cc082..17355d976 100644 --- a/checkstyle-override.xml +++ b/checkstyle-override.xml @@ -1,13 +1,13 @@ + "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" + "https://checkstyle.org/dtds/configuration_1_3.dtd"> - + 4.0.0 org.mybatis mybatis-parent - 31 + 51 + org.mybatis.dynamic-sql mybatis-dynamic-sql - 1.1.4 + 2.0.0-SNAPSHOT + MyBatis Dynamic SQL MyBatis framework for generating dynamic SQL 2016 + https://www.mybatis.org/mybatis-dynamic-sql/ + + + scm:git:ssh://git@github.com/mybatis/mybatis-dynamic-sql.git + scm:git:ssh://git@github.com/mybatis/mybatis-dynamic-sql.git + HEAD + https://github.com/mybatis/mybatis-dynamic-sql/ + + + GitHub Issue Management + https://github.com/mybatis/mybatis-dynamic-sql/issues + + + Github + https://github.com/mybatis/mybatis-dynamic-sql/actions + + + + gh-pages-scm + MyBatis Dynamic SQL GitHub Pages + scm:git:ssh://git@github.com/mybatis/mybatis-dynamic-sql.git + + - 1.8 - ${java.version} - ${java.version} - 5.5.2 - 1.5.2 - 4.2.0.RELEASE - 1.1.0 + 17 + 17 + 17 + 17 + 6.0.1 + 5.2.4 + + checkstyle-override.xml + + 1.5.0 + org.mybatis.dynamic.sql - 1.3.60 - 1.8 + + 2.2.21 + 17 + 2.0 + 2.0 + pom.xml,src/main/java,src/main/kotlin src/test/java,src/test/kotlin - 0.8.4 + + http://localhost:9000 + official + 2.0.1 + org.mybatis.dynamic.sql.*;version=${project.version};-noimport:=true + + + 1717449335 + + + org.jspecify + jspecify + 1.0.0 + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + provided + true + + + org.springframework + spring-jdbc + 6.2.12 + provided + true + + + org.mybatis + mybatis + 3.5.19 + provided + true + + + + org.jetbrains.kotlin + kotlin-compiler + ${kotlin.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-params + ${junit.jupiter.version} + test + + + org.assertj + assertj-core + 3.27.6 + test + + + org.mybatis + mybatis-spring + 3.0.5 + test + + + org.hsqldb + hsqldb + 2.7.4 + test + + + org.springframework.batch + spring-batch-core + ${spring.batch.version} + test + + + org.springframework.batch + spring-batch-test + ${spring.batch.version} + test + + + junit + junit + + + + + ch.qos.logback + logback-classic + 1.5.21 + test + + + org.testcontainers + testcontainers-junit-jupiter + ${test.containers.version} + test + + + org.testcontainers + testcontainers-postgresql + ${test.containers.version} + test + + + org.postgresql + postgresql + 42.7.8 + test + + + org.testcontainers + testcontainers-mariadb + ${test.containers.version} + test + + + org.mariadb.jdbc + mariadb-java-client + 3.5.6 + test + + + org.testcontainers + testcontainers-mysql + ${test.containers.version} + test + + + com.mysql + mysql-connector-j + 9.5.0 + test + + + - - - - org.apache.maven.plugins - maven-javadoc-plugin - - true - - - - + + org.jacoco + jacoco-maven-plugin + + + org/jetbrains/kotlin/**/* + + + + + org.apache.maven.plugins + maven-site-plugin + + true + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + + ${java.version} + + + + org.jetbrains.kotlin kotlin-maven-plugin @@ -107,30 +296,31 @@ java-compile - compile compile + compile java-test-compile - test-compile testCompile + test-compile + org.apache.maven.plugins maven-resources-plugin copy-changelog - pre-site copy-resources + pre-site ${project.build.directory}/generated-site/markdown/docs @@ -144,157 +334,32 @@ - org.eluder.coveralls + com.github.hazendaz.maven coveralls-maven-plugin src/main/java,src/main/kotlin - - - - - + - org.apache.maven.plugins - maven-checkstyle-plugin - - checkstyle-override.xml - + org.codehaus.mojo + build-helper-maven-plugin + 3.6.1 + + + + add-source + + generate-sources + + + ${project.basedir}/src/main/kotlin + + + + - - - - - org.jetbrains.kotlin - kotlin-stdlib-jdk8 - ${kotlin.version} - provided - - - org.springframework - spring-jdbc - 5.2.1.RELEASE - provided - - - - org.junit.jupiter - junit-jupiter-api - ${junit.jupiter.version} - test - - - org.junit.jupiter - junit-jupiter-engine - ${junit.jupiter.version} - test - - - org.junit.platform - junit-platform-launcher - ${junit.platform.version} - test - - - org.assertj - assertj-core - 3.14.0 - test - - - org.mybatis - mybatis - 3.5.3 - test - - - org.mybatis - mybatis-spring - 2.0.3 - test - - - org.hsqldb - hsqldb - 2.5.0 - test - - - org.springframework.batch - spring-batch-core - ${spring.batch.version} - test - - - org.springframework.batch - spring-batch-test - ${spring.batch.version} - test - - - junit - junit - - - - - ch.qos.logback - logback-classic - 1.2.3 - test - - - - org.hamcrest - hamcrest - 2.2 - test - - - - - https://github.com/mybatis/mybatis-dynamic-sql - scm:git:ssh://github.com/mybatis/mybatis-dynamic-sql.git - scm:git:ssh://git@github.com/mybatis/mybatis-dynamic-sql.git - mybatis-dynamic-sql-1.1.4 - - - GitHub Issue Management - https://github.com/mybatis/mybatis-dynamic-sql/issues - - - Travis CI - https://travis-ci.org/mybatis/mybatis-dynamic-sql - - - - gh-pages - MyBatis Dynamic SQL GitHub Pages - git:ssh://git@github.com/mybatis/mybatis-dynamic-sql.git?gh-pages# - - + - - - javadocVersion - - [9,) - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - -html5 - - - - - - - diff --git a/renovate.json b/renovate.json new file mode 100644 index 000000000..5db72dd6a --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" + ] +} diff --git a/src/main/java/org/mybatis/dynamic/sql/AbstractColumnComparisonCondition.java b/src/main/java/org/mybatis/dynamic/sql/AbstractColumnComparisonCondition.java index bacd1953a..65e45aafe 100644 --- a/src/main/java/org/mybatis/dynamic/sql/AbstractColumnComparisonCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/AbstractColumnComparisonCondition.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,24 +15,23 @@ */ package org.mybatis.dynamic.sql; -import org.mybatis.dynamic.sql.render.TableAliasCalculator; +import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore; -public abstract class AbstractColumnComparisonCondition implements VisitableCondition { +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; - protected BasicColumn column; - - protected AbstractColumnComparisonCondition(BasicColumn column) { - this.column = column; +public abstract class AbstractColumnComparisonCondition implements RenderableCondition { + + protected final BasicColumn rightColumn; + + protected AbstractColumnComparisonCondition(BasicColumn rightColumn) { + this.rightColumn = rightColumn; } + public abstract String operator(); + @Override - public R accept(ConditionVisitor visitor) { - return visitor.visit(this); - } - - public String renderCondition(String columnName, TableAliasCalculator tableAliasCalculator) { - return renderCondition(columnName, column.renderWithTableAlias(tableAliasCalculator)); + public FragmentAndParameters renderCondition(RenderingContext renderingContext, BindableColumn leftColumn) { + return rightColumn.render(renderingContext).mapFragment(f -> operator() + spaceBefore(f)); } - - protected abstract String renderCondition(String leftColumn, String rightColumn); } diff --git a/src/main/java/org/mybatis/dynamic/sql/AbstractListValueCondition.java b/src/main/java/org/mybatis/dynamic/sql/AbstractListValueCondition.java index e7544e9f9..41c6a56e2 100644 --- a/src/main/java/org/mybatis/dynamic/sql/AbstractListValueCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/AbstractListValueCondition.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,34 +15,129 @@ */ package org.mybatis.dynamic.sql; -import java.util.ArrayList; import java.util.Collection; import java.util.Objects; import java.util.function.Function; -import java.util.function.UnaryOperator; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; import java.util.stream.Stream; -public abstract class AbstractListValueCondition implements VisitableCondition { - protected Collection values; - protected UnaryOperator> valueStreamTransformer; +import org.mybatis.dynamic.sql.render.RenderedParameterInfo; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.FragmentCollector; + +public abstract class AbstractListValueCondition implements RenderableCondition { + protected final Collection values; protected AbstractListValueCondition(Collection values) { - this(values, UnaryOperator.identity()); + this.values = Objects.requireNonNull(values); + } + + public final Stream values() { + return values.stream(); + } + + @Override + public boolean isEmpty() { + return values.isEmpty(); + } + + private Collection applyMapper(Function mapper) { + Objects.requireNonNull(mapper); + return values().map(mapper).collect(Collectors.toList()); + } + + private Collection applyFilter(Predicate predicate) { + Objects.requireNonNull(predicate); + return values().filter(predicate).toList(); } - protected AbstractListValueCondition(Collection values, UnaryOperator> valueStreamTransformer) { - this.values = new ArrayList<>(Objects.requireNonNull(values)); - this.valueStreamTransformer = Objects.requireNonNull(valueStreamTransformer); + protected > S filterSupport(Predicate predicate, + Function, S> constructor, S self, Supplier emptySupplier) { + if (isEmpty()) { + return self; + } else { + Collection filtered = applyFilter(predicate); + return filtered.isEmpty() ? emptySupplier.get() : constructor.apply(filtered); + } } - - public final Stream mapValues(Function mapper) { - return valueStreamTransformer.apply(values.stream()).map(mapper); + + protected > S mapSupport(Function mapper, + Function, S> constructor, Supplier emptySupplier) { + if (isEmpty()) { + return emptySupplier.get(); + } else { + return constructor.apply(applyMapper(mapper)); + } } - + + public abstract String operator(); + @Override - public R accept(ConditionVisitor visitor) { - return visitor.visit(this); + public FragmentAndParameters renderCondition(RenderingContext renderingContext, BindableColumn leftColumn) { + return values().map(v -> toFragmentAndParameters(v, renderingContext, leftColumn)) + .collect(FragmentCollector.collect()) + .toFragmentAndParameters(Collectors.joining(",", //$NON-NLS-1$ + operator() + " (", ")")); //$NON-NLS-1$ //$NON-NLS-2$ } - public abstract String renderCondition(String columnName, Stream placeholders); + private FragmentAndParameters toFragmentAndParameters(T value, RenderingContext renderingContext, + BindableColumn leftColumn) { + RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo(leftColumn); + return FragmentAndParameters.withFragment(parameterInfo.renderedPlaceHolder()) + .withParameter(parameterInfo.parameterMapKey(), leftColumn.convertParameterType(value)) + .build(); + } + + /** + * Conditions may implement Filterable to add optionality to rendering. + * + *

If a condition is Filterable, then a user may add a filter to the usage of the condition that makes a decision + * whether to render the condition at runtime. Conditions that fail the filter will be dropped from the + * rendered SQL. + * + *

Implementations of Filterable may call + * {@link AbstractListValueCondition#filterSupport(Predicate, Function, AbstractListValueCondition, Supplier)} as + * a common implementation of the filtering algorithm. + * + * @param the Java type related to the database column type + */ + public interface Filterable { + /** + * If renderable and the value matches the predicate, returns this condition. Else returns a condition + * that will not render. + * + * @param predicate predicate applied to the value, if renderable + * @return this condition if renderable and the value matches the predicate, otherwise a condition + * that will not render. + */ + AbstractListValueCondition filter(Predicate predicate); + } + + /** + * Conditions may implement Mappable to alter condition values or types during rendering. + * + *

If a condition is Mappable, then a user may add a mapper to the usage of the condition that can alter the + * values of a condition, or change that datatype. + * + *

Implementations of Mappable may call + * {@link AbstractListValueCondition#mapSupport(Function, Function, Supplier)} as + * a common implementation of the mapping algorithm. + * + * @param the Java type related to the database column type + */ + public interface Mappable { + /** + * If renderable, apply the mapping to the value and return a new condition with the new value. Else return a + * condition that will not render (this). + * + * @param mapper a mapping function to apply to the value, if renderable + * @param type of the new condition + * @return a new condition with the result of applying the mapper to the value of this condition, + * if renderable, otherwise a condition that will not render. + */ + AbstractListValueCondition map(Function mapper); + } } diff --git a/src/main/java/org/mybatis/dynamic/sql/AbstractNoValueCondition.java b/src/main/java/org/mybatis/dynamic/sql/AbstractNoValueCondition.java index 9d483742f..71daa7763 100644 --- a/src/main/java/org/mybatis/dynamic/sql/AbstractNoValueCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/AbstractNoValueCondition.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,30 +15,54 @@ */ package org.mybatis.dynamic.sql; -import java.util.Objects; import java.util.function.BooleanSupplier; +import java.util.function.Supplier; -public abstract class AbstractNoValueCondition implements VisitableCondition { +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; - private BooleanSupplier booleanSupplier; - - protected AbstractNoValueCondition() { - booleanSupplier = () -> true; - } - - protected AbstractNoValueCondition(BooleanSupplier booleanSupplier) { - this.booleanSupplier = Objects.requireNonNull(booleanSupplier); +public abstract class AbstractNoValueCondition implements RenderableCondition { + + protected > S filterSupport(BooleanSupplier booleanSupplier, + Supplier emptySupplier, S self) { + if (isEmpty()) { + return self; + } else { + return booleanSupplier.getAsBoolean() ? self : emptySupplier.get(); + } } - + + public abstract String operator(); + @Override - public boolean shouldRender() { - return booleanSupplier.getAsBoolean(); + public FragmentAndParameters renderCondition(RenderingContext renderingContext, BindableColumn leftColumn) { + return FragmentAndParameters.fromFragment(operator()); } - - @Override - public R accept(ConditionVisitor visitor) { - return visitor.visit(this); + + /** + * Conditions may implement Filterable to add optionality to rendering. + * + *

If a condition is Filterable, then a user may add a filter to the usage of the condition that makes a decision + * whether to render the condition at runtime. Conditions that fail the filter will be dropped from the + * rendered SQL. + * + *

Implementations of Filterable may call + * {@link AbstractNoValueCondition#filterSupport(BooleanSupplier, Supplier, AbstractNoValueCondition)} as + * a common implementation of the filtering algorithm. + */ + public interface Filterable { + /** + * If renderable and the supplier returns true, returns this condition. Else returns a condition that will not + * render. + * + * @param booleanSupplier + * function that specifies whether the condition should render + * @param + * condition type - not used except for compilation compliance + * + * @return this condition if renderable and the supplier returns true, otherwise a condition that will not + * render. + */ + AbstractNoValueCondition filter(BooleanSupplier booleanSupplier); } - - public abstract String renderCondition(String columnName); } diff --git a/src/main/java/org/mybatis/dynamic/sql/AbstractSingleValueCondition.java b/src/main/java/org/mybatis/dynamic/sql/AbstractSingleValueCondition.java index fc6fc4630..c16dbf08c 100644 --- a/src/main/java/org/mybatis/dynamic/sql/AbstractSingleValueCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/AbstractSingleValueCondition.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,37 +15,104 @@ */ package org.mybatis.dynamic.sql; -import java.util.Objects; +import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore; + +import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; -public abstract class AbstractSingleValueCondition implements VisitableCondition { - protected Supplier valueSupplier; - private Predicate predicate; - - protected AbstractSingleValueCondition(Supplier valueSupplier) { - this.valueSupplier = Objects.requireNonNull(valueSupplier); - predicate = v -> true; - } - - protected AbstractSingleValueCondition(Supplier valueSupplier, Predicate predicate) { - this.valueSupplier = Objects.requireNonNull(valueSupplier); - this.predicate = Objects.requireNonNull(predicate); +import org.mybatis.dynamic.sql.render.RenderedParameterInfo; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + +public abstract class AbstractSingleValueCondition implements RenderableCondition { + protected final T value; + + protected AbstractSingleValueCondition(T value) { + this.value = value; } - + public T value() { - return valueSupplier.get(); + return value; } - - @Override - public boolean shouldRender() { - return predicate.test(value()); + + protected > S filterSupport(Predicate predicate, + Supplier emptySupplier, S self) { + if (isEmpty()) { + return self; + } else { + return predicate.test(value) ? self : emptySupplier.get(); + } + } + + protected > S mapSupport(Function mapper, + Function constructor, Supplier emptySupplier) { + if (isEmpty()) { + return emptySupplier.get(); + } else { + return constructor.apply(mapper.apply(value)); + } } - + + public abstract String operator(); + @Override - public R accept(ConditionVisitor visitor) { - return visitor.visit(this); + public FragmentAndParameters renderCondition(RenderingContext renderingContext, BindableColumn leftColumn) { + RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo(leftColumn); + String finalFragment = operator() + spaceBefore(parameterInfo.renderedPlaceHolder()); + + return FragmentAndParameters.withFragment(finalFragment) + .withParameter(parameterInfo.parameterMapKey(), leftColumn.convertParameterType(value())) + .build(); + } + + /** + * Conditions may implement Filterable to add optionality to rendering. + * + *

If a condition is Filterable, then a user may add a filter to the usage of the condition that makes a decision + * whether to render the condition at runtime. Conditions that fail the filter will be dropped from the + * rendered SQL. + * + *

Implementations of Filterable may call + * {@link AbstractSingleValueCondition#filterSupport(Predicate, Supplier, AbstractSingleValueCondition)} as + * a common implementation of the filtering algorithm. + * + * @param the Java type related to the database column type + */ + public interface Filterable { + /** + * If renderable and the value matches the predicate, returns this condition. Else returns a condition + * that will not render. + * + * @param predicate predicate applied to the value, if renderable + * @return this condition if renderable and the value matches the predicate, otherwise a condition + * that will not render. + */ + AbstractSingleValueCondition filter(Predicate predicate); + } + + /** + * Conditions may implement Mappable to alter condition values or types during rendering. + * + *

If a condition is Mappable, then a user may add a mapper to the usage of the condition that can alter the + * values of a condition, or change that datatype. + * + *

Implementations of Mappable may call + * {@link AbstractSingleValueCondition#mapSupport(Function, Function, Supplier)} as + * a common implementation of the mapping algorithm. + * + * @param the Java type related to the database column type + */ + public interface Mappable { + /** + * If renderable, apply the mapping to the value and return a new condition with the new value. Else return a + * condition that will not render (this). + * + * @param mapper a mapping function to apply to the value, if renderable + * @param type of the new condition + * @return a new condition with the result of applying the mapper to the value of this condition, + * if renderable, otherwise a condition that will not render. + */ + AbstractSingleValueCondition map(Function mapper); } - - public abstract String renderCondition(String columnName, String placeholder); } diff --git a/src/main/java/org/mybatis/dynamic/sql/AbstractSubselectCondition.java b/src/main/java/org/mybatis/dynamic/sql/AbstractSubselectCondition.java index 44de4f445..dcfbd4b3c 100644 --- a/src/main/java/org/mybatis/dynamic/sql/AbstractSubselectCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/AbstractSubselectCondition.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,24 +15,28 @@ */ package org.mybatis.dynamic.sql; +import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.select.SelectModel; +import org.mybatis.dynamic.sql.select.render.SubQueryRenderer; import org.mybatis.dynamic.sql.util.Buildable; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + +public abstract class AbstractSubselectCondition implements RenderableCondition { + private final SelectModel selectModel; -public abstract class AbstractSubselectCondition implements VisitableCondition { - private SelectModel selectModel; - protected AbstractSubselectCondition(Buildable selectModelBuilder) { this.selectModel = selectModelBuilder.build(); } - - public SelectModel selectModel() { - return selectModel; - } + + public abstract String operator(); @Override - public R accept(ConditionVisitor visitor) { - return visitor.visit(this); + public FragmentAndParameters renderCondition(RenderingContext renderingContext, BindableColumn leftColumn) { + return SubQueryRenderer.withSelectModel(selectModel) + .withRenderingContext(renderingContext) + .withPrefix(operator() + " (") //$NON-NLS-1$ + .withSuffix(")") //$NON-NLS-1$ + .build() + .render(); } - - public abstract String renderCondition(String columnName, String renderedSelectStatement); } diff --git a/src/main/java/org/mybatis/dynamic/sql/AbstractTwoValueCondition.java b/src/main/java/org/mybatis/dynamic/sql/AbstractTwoValueCondition.java index d31333fd0..6cceff16e 100644 --- a/src/main/java/org/mybatis/dynamic/sql/AbstractTwoValueCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/AbstractTwoValueCondition.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,44 +15,150 @@ */ package org.mybatis.dynamic.sql; -import java.util.Objects; +import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore; + +import java.util.function.BiFunction; import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.function.Predicate; import java.util.function.Supplier; -public abstract class AbstractTwoValueCondition implements VisitableCondition { - protected Supplier valueSupplier1; - protected Supplier valueSupplier2; - private BiPredicate predicate; - - protected AbstractTwoValueCondition(Supplier valueSupplier1, Supplier valueSupplier2) { - this.valueSupplier1 = Objects.requireNonNull(valueSupplier1); - this.valueSupplier2 = Objects.requireNonNull(valueSupplier2); - predicate = (v1, v2) -> true; - } +import org.mybatis.dynamic.sql.render.RenderedParameterInfo; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + +public abstract class AbstractTwoValueCondition implements RenderableCondition { + protected final T value1; + protected final T value2; - protected AbstractTwoValueCondition(Supplier valueSupplier1, Supplier valueSupplier2, - BiPredicate predicate) { - this(valueSupplier1, valueSupplier2); - this.predicate = Objects.requireNonNull(predicate); + protected AbstractTwoValueCondition(T value1, T value2) { + this.value1 = value1; + this.value2 = value2; } public T value1() { - return valueSupplier1.get(); + return value1; } public T value2() { - return valueSupplier2.get(); + return value2; } - - @Override - public boolean shouldRender() { - return predicate.test(value1(), value2()); + + protected > S filterSupport(BiPredicate predicate, + Supplier emptySupplier, S self) { + if (isEmpty()) { + return self; + } else { + return predicate.test(value1, value2) ? self : emptySupplier.get(); + } } - + + protected > S filterSupport(Predicate predicate, + Supplier emptySupplier, S self) { + return filterSupport((v1, v2) -> predicate.test(v1) && predicate.test(v2), emptySupplier, self); + } + + protected > S mapSupport(Function mapper1, + Function mapper2, BiFunction constructor, Supplier emptySupplier) { + if (isEmpty()) { + return emptySupplier.get(); + } else { + return constructor.apply(mapper1.apply(value1), mapper2.apply(value2)); + } + } + + public abstract String operator1(); + + public abstract String operator2(); + @Override - public R accept(ConditionVisitor visitor) { - return visitor.visit(this); + public FragmentAndParameters renderCondition(RenderingContext renderingContext, BindableColumn leftColumn) { + RenderedParameterInfo parameterInfo1 = renderingContext.calculateParameterInfo(leftColumn); + RenderedParameterInfo parameterInfo2 = renderingContext.calculateParameterInfo(leftColumn); + + String finalFragment = operator1() + + spaceBefore(parameterInfo1.renderedPlaceHolder()) + + spaceBefore(operator2()) + + spaceBefore(parameterInfo2.renderedPlaceHolder()); + + return FragmentAndParameters.withFragment(finalFragment) + .withParameter(parameterInfo1.parameterMapKey(), leftColumn.convertParameterType(value1())) + .withParameter(parameterInfo2.parameterMapKey(), leftColumn.convertParameterType(value2())) + .build(); } - public abstract String renderCondition(String columnName, String placeholder1, String placeholder2); + /** + * Conditions may implement Filterable to add optionality to rendering. + * + *

If a condition is Filterable, then a user may add a filter to the usage of the condition that makes a decision + * whether to render the condition at runtime. Conditions that fail the filter will be dropped from the + * rendered SQL. + * + *

Implementations of Filterable may call + * {@link AbstractTwoValueCondition#filterSupport(Predicate, Supplier, AbstractTwoValueCondition)} + * or {@link AbstractTwoValueCondition#filterSupport(BiPredicate, Supplier, AbstractTwoValueCondition)} as + * a common implementation of the filtering algorithm. + * + * @param the Java type related to the database column type + */ + public interface Filterable { + /** + * If renderable and the values match the predicate, returns this condition. Else returns a condition + * that will not render. + * + * @param predicate predicate applied to the values, if renderable + * @return this condition if renderable and the values match the predicate, otherwise a condition + * that will not render. + */ + AbstractTwoValueCondition filter(BiPredicate predicate); + + /** + * If renderable and both values match the predicate, returns this condition. Else returns a condition + * that will not render. This function implements a short-circuiting test. If the + * first value does not match the predicate, then the second value will not be tested. + * + * @param predicate predicate applied to both values, if renderable + * @return this condition if renderable and the values match the predicate, otherwise a condition + * that will not render. + */ + AbstractTwoValueCondition filter(Predicate predicate); + } + + /** + * Conditions may implement Mappable to alter condition values or types during rendering. + * + *

If a condition is Mappable, then a user may add a mapper to the usage of the condition that can alter the + * values of a condition, or change that datatype. + * + *

Implementations of Mappable may call + * {@link AbstractTwoValueCondition#mapSupport(Function, Function, BiFunction, Supplier)} as + * a common implementation of the mapping algorithm. + * + * @param the Java type related to the database column type + */ + public interface Mappable { + /** + * If renderable, apply the mappings to the values and return a new condition with the new values. Else return a + * condition that will not render (this). + * + * @param mapper1 a mapping function to apply to the first value, if renderable + * @param mapper2 a mapping function to apply to the second value, if renderable + * @param type of the new condition + * @return a new condition with the result of applying the mappers to the values of this condition, + * if renderable, otherwise a condition that will not render. + */ + AbstractTwoValueCondition map(Function mapper1, + Function mapper2); + + /** + * If renderable, apply the mapping to both values and return a new condition with the new values. Else return a + * condition that will not render (this). + * + * @param mapper a mapping function to apply to both values, if renderable + * @param type of the new condition + * @return a new condition with the result of applying the mappers to the values of this condition, + * if renderable, otherwise a condition that will not render. + */ + AbstractTwoValueCondition map(Function mapper); + } } diff --git a/src/main/java/org/mybatis/dynamic/sql/AliasableSqlTable.java b/src/main/java/org/mybatis/dynamic/sql/AliasableSqlTable.java new file mode 100644 index 000000000..915b6a561 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/AliasableSqlTable.java @@ -0,0 +1,61 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; + +import org.jspecify.annotations.Nullable; + +public abstract class AliasableSqlTable> extends SqlTable { + + private @Nullable String tableAlias; + private final Supplier constructor; + + protected AliasableSqlTable(String tableName, Supplier constructor) { + super(tableName); + this.constructor = Objects.requireNonNull(constructor); + } + + public T withAlias(String alias) { + T newTable = constructor.get(); + ((AliasableSqlTable) newTable).tableAlias = alias; + newTable.tableName = tableName; + return newTable; + } + + /** + * Returns a new instance of this table with the specified name. All column instances are recreated. + * This is useful for sharding where the table name may change at runtime based on some sharding algorithm, + * but all other table attributes are the same. + * + * @param name new name for the table + * @return a new AliasableSqlTable with the specified name, all other table attributes are copied + */ + public T withName(String name) { + Objects.requireNonNull(name); + T newTable = constructor.get(); + ((AliasableSqlTable) newTable).tableAlias = tableAlias; + newTable.tableName = name; + return newTable; + } + + @Override + public Optional tableAlias() { + return Optional.ofNullable(tableAlias); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/AndOrCriteriaGroup.java b/src/main/java/org/mybatis/dynamic/sql/AndOrCriteriaGroup.java new file mode 100644 index 000000000..ff630a036 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/AndOrCriteriaGroup.java @@ -0,0 +1,82 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.jspecify.annotations.Nullable; + +/** + * This class represents a criteria group with either an AND or an OR connector. + * This class is intentionally NOT derived from SqlCriterion because we only want it to be + * available where an AND or an OR condition is appropriate. + * + * @author Jeff Butler + * + * @since 1.4.0 + */ +public class AndOrCriteriaGroup { + private final String connector; + private final @Nullable SqlCriterion initialCriterion; + private final List subCriteria; + + private AndOrCriteriaGroup(Builder builder) { + connector = Objects.requireNonNull(builder.connector); + initialCriterion = builder.initialCriterion; + subCriteria = builder.subCriteria; + } + + public String connector() { + return connector; + } + + public Optional initialCriterion() { + return Optional.ofNullable(initialCriterion); + } + + public List subCriteria() { + return Collections.unmodifiableList(subCriteria); + } + + public static class Builder { + private @Nullable String connector; + private @Nullable SqlCriterion initialCriterion; + private final List subCriteria = new ArrayList<>(); + + public Builder withConnector(String connector) { + this.connector = connector; + return this; + } + + public Builder withInitialCriterion(@Nullable SqlCriterion initialCriterion) { + this.initialCriterion = initialCriterion; + return this; + } + + public Builder withSubCriteria(List subCriteria) { + this.subCriteria.addAll(subCriteria); + return this; + } + + public AndOrCriteriaGroup build() { + return new AndOrCriteriaGroup(this); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/BasicColumn.java b/src/main/java/org/mybatis/dynamic/sql/BasicColumn.java index 80b8e0d70..2573b4529 100644 --- a/src/main/java/org/mybatis/dynamic/sql/BasicColumn.java +++ b/src/main/java/org/mybatis/dynamic/sql/BasicColumn.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,66 +15,71 @@ */ package org.mybatis.dynamic.sql; +import java.sql.JDBCType; import java.util.Optional; -import org.mybatis.dynamic.sql.render.TableAliasCalculator; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.render.RenderingStrategy; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; /** * Describes attributes of columns that are necessary for rendering if the column is not expected to * be bound as a JDBC parameter. Columns in select lists, join expressions, and group by expressions * are typically not bound. - * - * @author Jeff Butler * + * @author Jeff Butler */ public interface BasicColumn { /** * Returns the columns alias if one has been specified. - * + * * @return the column alias */ Optional alias(); - + /** * Returns a new instance of a BasicColumn with the alias set. - * - * @param alias the column alias to set + * + * @param alias + * the column alias to set + * * @return new instance with alias set */ BasicColumn as(String alias); - - /** - * Returns the name of the item aliased with a table name if appropriate. - * For example, "a.foo". This is appropriate for where clauses and order by clauses. - * - * @param tableAliasCalculator the table alias calculator for the current renderer - * @return the item name with the table alias applied - */ - String renderWithTableAlias(TableAliasCalculator tableAliasCalculator); - + /** - * Returns the name of the item aliased with a table name and column alias if appropriate. - * For example, "a.foo as bar". This is appropriate for select list clauses. - * - * @param tableAliasCalculator the table alias calculator for the current renderer - * @return the item name with the table and column aliases applied + * Returns a rendering of the column. + * The rendered fragment should include the table alias based on the TableAliasCalculator + * in the RenderingContext. The fragment could contain prepared statement parameter + * markers and associated parameter values if desired. + * + * @param renderingContext the rendering context (strategy, sequence, etc.) + * @return a rendered SQL fragment and, optionally, parameters associated with the fragment + * @since 1.5.1 */ - default String renderWithTableAndColumnAlias(TableAliasCalculator tableAliasCalculator) { - String nameAndTableAlias = renderWithTableAlias(tableAliasCalculator); - - return alias().map(a -> nameAndTableAlias + " as " + a) //$NON-NLS-1$ - .orElse(nameAndTableAlias); + FragmentAndParameters render(RenderingContext renderingContext); + + default Optional jdbcType() { + return Optional.empty(); + } + + default Optional typeHandler() { + return Optional.empty(); } - + + default Optional renderingStrategy() { + return Optional.empty(); + } + /** * Utility method to make it easier to build column lists for methods that require an * array rather than the varargs method. - * + * * @param columns list of BasicColumn * @return an array of BasicColumn */ - static BasicColumn[] columnList(BasicColumn...columns) { + static BasicColumn[] columnList(BasicColumn... columns) { return columns; } } diff --git a/src/main/java/org/mybatis/dynamic/sql/BindableColumn.java b/src/main/java/org/mybatis/dynamic/sql/BindableColumn.java index 00c53364a..0fd90b7c8 100644 --- a/src/main/java/org/mybatis/dynamic/sql/BindableColumn.java +++ b/src/main/java/org/mybatis/dynamic/sql/BindableColumn.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,28 +15,32 @@ */ package org.mybatis.dynamic.sql; -import java.sql.JDBCType; import java.util.Optional; +import org.jspecify.annotations.Nullable; + /** - * Describes additional attributes of columns that are necessary for binding the column as a JDBC parameter. - * Columns in where clauses are typically bound. - * + * Describes a column with a known data type. The type is only used by the compiler to assure type safety + * when building clauses with conditions. + * * @author Jeff Butler * - * @param - even though the type is not directly used in this class, - * it is used by the compiler to match columns with conditions so it should - * not be removed. -*/ + * @param + * - the Java type that corresponds to this column + */ public interface BindableColumn extends BasicColumn { /** - * Override the base method definition to make it more specific to this interface. + * Override the base method definition to make it more specific to this interface. */ @Override BindableColumn as(String alias); - Optional jdbcType(); - - Optional typeHandler(); + default @Nullable Object convertParameterType(T value) { + return value; + } + + default Optional> javaType() { + return Optional.empty(); + } } diff --git a/src/main/java/org/mybatis/dynamic/sql/BoundValue.java b/src/main/java/org/mybatis/dynamic/sql/BoundValue.java new file mode 100644 index 000000000..5151a5dad --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/BoundValue.java @@ -0,0 +1,71 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql; + +import java.util.Objects; +import java.util.Optional; + +import org.mybatis.dynamic.sql.exception.InvalidSqlException; +import org.mybatis.dynamic.sql.render.RenderedParameterInfo; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.Messages; + +/** + * BoundValues are added to rendered SQL as a parameter marker only. + * + *

BoundValues are most useful in the context of functions. For example, a column value could be + * incremented with an update statement like this: + * + * UpdateStatementProvider updateStatement = update(person) + * .set(age).equalTo(add(age, value(1))) + * .where(id, isEqualTo(5)) + * .build() + * .render(RenderingStrategies.MYBATIS3); + * + * + * @param the column type + * @since 1.5.1 + */ +public class BoundValue implements BindableColumn { + private final T value; + + private BoundValue(T value) { + this.value = Objects.requireNonNull(value); + } + + @Override + public FragmentAndParameters render(RenderingContext renderingContext) { + RenderedParameterInfo rpi = renderingContext.calculateParameterInfo(this); + return FragmentAndParameters.withFragment(rpi.renderedPlaceHolder()) + .withParameter(rpi.parameterMapKey(), value) + .build(); + } + + @Override + public Optional alias() { + return Optional.empty(); + } + + @Override + public BoundValue as(String alias) { + throw new InvalidSqlException(Messages.getString("ERROR.38")); //$NON-NLS-1$ + } + + public static BoundValue of(T value) { + return new BoundValue<>(value); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/ColumnAndConditionCriterion.java b/src/main/java/org/mybatis/dynamic/sql/ColumnAndConditionCriterion.java new file mode 100644 index 000000000..053c18f64 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/ColumnAndConditionCriterion.java @@ -0,0 +1,72 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql; + +import java.util.Objects; + +import org.jspecify.annotations.Nullable; + +public class ColumnAndConditionCriterion extends SqlCriterion { + private final BindableColumn column; + private final RenderableCondition condition; + + private ColumnAndConditionCriterion(Builder builder) { + super(builder); + column = Objects.requireNonNull(builder.column); + condition = Objects.requireNonNull(builder.condition); + } + + public BindableColumn column() { + return column; + } + + public RenderableCondition condition() { + return condition; + } + + @Override + public R accept(SqlCriterionVisitor visitor) { + return visitor.visit(this); + } + + public static Builder withColumn(BindableColumn column) { + return new Builder().withColumn(column); + } + + public static class Builder extends AbstractBuilder> { + private @Nullable BindableColumn column; + private @Nullable RenderableCondition condition; + + public Builder withColumn(BindableColumn column) { + this.column = column; + return this; + } + + public Builder withCondition(RenderableCondition condition) { + this.condition = condition; + return this; + } + + @Override + protected Builder getThis() { + return this; + } + + public ColumnAndConditionCriterion build() { + return new ColumnAndConditionCriterion<>(this); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/Constant.java b/src/main/java/org/mybatis/dynamic/sql/Constant.java index e6a56bf54..90547765f 100644 --- a/src/main/java/org/mybatis/dynamic/sql/Constant.java +++ b/src/main/java/org/mybatis/dynamic/sql/Constant.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -18,15 +18,22 @@ import java.util.Objects; import java.util.Optional; -import org.mybatis.dynamic.sql.render.TableAliasCalculator; +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; -public class Constant implements BasicColumn { +public class Constant implements BindableColumn { + + private final @Nullable String alias; + private final String value; - private String alias; - private String value; - private Constant(String value) { + this(value, null); + } + + private Constant(String value, @Nullable String alias) { this.value = Objects.requireNonNull(value); + this.alias = alias; } @Override @@ -35,18 +42,16 @@ public Optional alias() { } @Override - public String renderWithTableAlias(TableAliasCalculator tableAliasCalculator) { - return value; + public FragmentAndParameters render(RenderingContext renderingContext) { + return FragmentAndParameters.fromFragment(value); } @Override - public Constant as(String alias) { - Constant copy = new Constant(value); - copy.alias = alias; - return copy; + public Constant as(String alias) { + return new Constant<>(value, alias); } - - public static Constant of(String value) { - return new Constant(value); + + public static Constant of(String value) { + return new Constant<>(value); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/CriteriaGroup.java b/src/main/java/org/mybatis/dynamic/sql/CriteriaGroup.java new file mode 100644 index 000000000..476cb6b3f --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/CriteriaGroup.java @@ -0,0 +1,67 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql; + +import java.util.Optional; + +import org.jspecify.annotations.Nullable; + +/** + * This class represents a criteria group without an AND or an OR connector. This is useful + * in situations where the initial SqlCriterion in a list should be further grouped + * as in an expression like ((A < 5 and B > 6) or C = 3) + * + * @author Jeff Butler, inspired by @JoshuaJeme + * + * @since 1.4.0 + */ +public class CriteriaGroup extends SqlCriterion { + private final @Nullable SqlCriterion initialCriterion; + + protected CriteriaGroup(AbstractGroupBuilder builder) { + super(builder); + initialCriterion = builder.initialCriterion; + } + + public Optional initialCriterion() { + return Optional.ofNullable(initialCriterion); + } + + @Override + public R accept(SqlCriterionVisitor visitor) { + return visitor.visit(this); + } + + public abstract static class AbstractGroupBuilder> extends AbstractBuilder { + private @Nullable SqlCriterion initialCriterion; + + public T withInitialCriterion(@Nullable SqlCriterion initialCriterion) { + this.initialCriterion = initialCriterion; + return getThis(); + } + } + + public static class Builder extends AbstractGroupBuilder { + public CriteriaGroup build() { + return new CriteriaGroup(this); + } + + @Override + protected Builder getThis() { + return this; + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/DerivedColumn.java b/src/main/java/org/mybatis/dynamic/sql/DerivedColumn.java new file mode 100644 index 000000000..df19b8d6b --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/DerivedColumn.java @@ -0,0 +1,132 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql; + +import java.sql.JDBCType; +import java.util.Objects; +import java.util.Optional; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + +/** + * A derived column is a column that is not directly related to a table. This is primarily + * used for supporting sub-queries. The main difference in this class and {@link SqlColumn} is + * that this class does not have a related {@link SqlTable} and therefore ignores any table + * qualifier set in a query. If a table qualifier is required it can be set directly in the + * builder for this class. + * + * @param + * The Java type that corresponds to this column - not used except for compiler type checking for conditions + */ +public class DerivedColumn implements BindableColumn { + private final String name; + private final @Nullable String tableQualifier; + private final @Nullable String columnAlias; + private final @Nullable JDBCType jdbcType; + private final @Nullable String typeHandler; + + protected DerivedColumn(Builder builder) { + this.name = Objects.requireNonNull(builder.name); + this.tableQualifier = builder.tableQualifier; + this.columnAlias = builder.columnAlias; + this.jdbcType = builder.jdbcType; + this.typeHandler = builder.typeHandler; + } + + @Override + public Optional alias() { + return Optional.ofNullable(columnAlias); + } + + @Override + public Optional jdbcType() { + return Optional.ofNullable(jdbcType); + } + + @Override + public Optional typeHandler() { + return Optional.ofNullable(typeHandler); + } + + @Override + public FragmentAndParameters render(RenderingContext renderingContext) { + String fragment = tableQualifier == null ? name : tableQualifier + "." + name; //$NON-NLS-1$ + return FragmentAndParameters.fromFragment(fragment); + } + + @Override + public DerivedColumn as(String columnAlias) { + return new Builder() + .withName(name) + .withColumnAlias(columnAlias) + .withJdbcType(jdbcType) + .withTypeHandler(typeHandler) + .withTableQualifier(tableQualifier) + .build(); + } + + public static DerivedColumn of(String name) { + return new Builder() + .withName(name) + .build(); + } + + public static DerivedColumn of(String name, String tableQualifier) { + return new Builder() + .withName(name) + .withTableQualifier(tableQualifier) + .build(); + } + + public static class Builder { + private @Nullable String name; + private @Nullable String tableQualifier; + private @Nullable String columnAlias; + private @Nullable JDBCType jdbcType; + private @Nullable String typeHandler; + + public Builder withName(String name) { + this.name = name; + return this; + } + + public Builder withTableQualifier(@Nullable String tableQualifier) { + this.tableQualifier = tableQualifier; + return this; + } + + public Builder withColumnAlias(String columnAlias) { + this.columnAlias = columnAlias; + return this; + } + + public Builder withJdbcType(@Nullable JDBCType jdbcType) { + this.jdbcType = jdbcType; + return this; + } + + public Builder withTypeHandler(@Nullable String typeHandler) { + this.typeHandler = typeHandler; + return this; + } + + public DerivedColumn build() { + return new DerivedColumn<>(this); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/ExistsCriterion.java b/src/main/java/org/mybatis/dynamic/sql/ExistsCriterion.java new file mode 100644 index 000000000..17cecdaa6 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/ExistsCriterion.java @@ -0,0 +1,56 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql; + +import java.util.Objects; + +import org.jspecify.annotations.Nullable; + +public class ExistsCriterion extends SqlCriterion { + private final ExistsPredicate existsPredicate; + + private ExistsCriterion(Builder builder) { + super(builder); + this.existsPredicate = Objects.requireNonNull(builder.existsPredicate); + } + + public ExistsPredicate existsPredicate() { + return existsPredicate; + } + + @Override + public R accept(SqlCriterionVisitor visitor) { + return visitor.visit(this); + } + + public static class Builder extends AbstractBuilder { + private @Nullable ExistsPredicate existsPredicate; + + public Builder withExistsPredicate(ExistsPredicate existsPredicate) { + this.existsPredicate = existsPredicate; + return this; + } + + public ExistsCriterion build() { + return new ExistsCriterion(this); + } + + @Override + protected Builder getThis() { + return this; + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/ExistsPredicate.java b/src/main/java/org/mybatis/dynamic/sql/ExistsPredicate.java new file mode 100644 index 000000000..c22a84aa2 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/ExistsPredicate.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql; + +import java.util.Objects; + +import org.mybatis.dynamic.sql.select.SelectModel; +import org.mybatis.dynamic.sql.util.Buildable; + +public class ExistsPredicate { + private final Buildable selectModelBuilder; + private final String operator; + + private ExistsPredicate(String operator, Buildable selectModelBuilder) { + this.selectModelBuilder = Objects.requireNonNull(selectModelBuilder); + this.operator = Objects.requireNonNull(operator); + } + + public String operator() { + return operator; + } + + public Buildable selectModelBuilder() { + return selectModelBuilder; + } + + public static ExistsPredicate exists(Buildable selectModelBuilder) { + return new ExistsPredicate("exists", selectModelBuilder); //$NON-NLS-1$ + } + + public static ExistsPredicate notExists(Buildable selectModelBuilder) { + return new ExistsPredicate("not exists", selectModelBuilder); //$NON-NLS-1$ + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/NotCriterion.java b/src/main/java/org/mybatis/dynamic/sql/NotCriterion.java new file mode 100644 index 000000000..f0a0010f8 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/NotCriterion.java @@ -0,0 +1,45 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql; + +/** + * This class represents a criteria group with a NOT. + * + * @author Jeff Butler + * + * @since 1.4.0 + */ +public class NotCriterion extends CriteriaGroup { + private NotCriterion(Builder builder) { + super(builder); + } + + @Override + public R accept(SqlCriterionVisitor visitor) { + return visitor.visit(this); + } + + public static class Builder extends AbstractGroupBuilder { + public NotCriterion build() { + return new NotCriterion(this); + } + + @Override + protected Builder getThis() { + return this; + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/ParameterTypeConverter.java b/src/main/java/org/mybatis/dynamic/sql/ParameterTypeConverter.java new file mode 100644 index 000000000..4f4856e19 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/ParameterTypeConverter.java @@ -0,0 +1,64 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql; + +import org.jspecify.annotations.Nullable; + +/** + * A parameter type converter is used to change a parameter value from one type to another + * during statement rendering and before the parameter is placed into the parameter map. This can be used + * to somewhat mimic the function of a MyBatis type handler for runtimes such as Spring that don't have + * a corresponding concept. + * + *

Since Spring does not have the concept of type handlers, it is a best practice to only use + * Java data types that have a clear correlation to SQL data types (for example Java String correlates + * automatically with VARCHAR). Using a parameter type converter will allow you to use data types in your + * model classes that would otherwise be difficult to use with Spring. + * + *

A parameter type converter is associated with a SqlColumn. + * + *

This interface is based on Spring's general Converter interface and is intentionally compatible with it. + * Existing converters may be reused if they are marked with this additional interface. + * + *

The converter is only used for parameters in a parameter map. It is not used for result set processing. + * It is also not used for insert statements that are based on an external row class. The converter will be called + * in the following circumstances: + * + *

    + *
  • Parameters in a general insert statement (for the Value and ValueWhenPresent mappings)
  • + *
  • Parameters in an update statement (for the Value and ValueWhenPresent mappings)
  • + *
  • Parameters in a where clause in any statement (for conditions that accept a value or multiple values)
  • + *
+ * + * @param Source Type + * @param Target Type + * + * @see SqlColumn + * @author Jeff Butler + * @since 1.1.5 + */ +@FunctionalInterface +public interface ParameterTypeConverter { + /** + * Convert the value from one value to another. + * + *

The input value will never be null - the framework will automatically handle nulls. + * + * @param source value as specified in the condition, or after a map operation. Never null. + * @return Possibly null converted value. + */ + @Nullable T convert(S source); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/RenderableCondition.java b/src/main/java/org/mybatis/dynamic/sql/RenderableCondition.java new file mode 100644 index 000000000..51dc912e8 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/RenderableCondition.java @@ -0,0 +1,81 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql; + +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + +@FunctionalInterface +public interface RenderableCondition { + /** + * Render a condition - typically a condition in a WHERE clause. + * + *

A rendered condition includes an SQL fragment, and any associated parameters. For example, + * the isEqual condition should be rendered as "= ?" where "?" is a properly formatted + * parameter marker (the parameter marker can be computed from the RenderingContext). + * Note that a rendered condition should NOT include the left side of the phrase - that is rendered + * by the {@link RenderableCondition#renderLeftColumn(RenderingContext, BindableColumn)} method. + * + * @param renderingContext the current rendering context + * @param leftColumn the column related to this condition in a where clause + * @return the rendered condition. Should NOT include the column. + */ + FragmentAndParameters renderCondition(RenderingContext renderingContext, BindableColumn leftColumn); + + /** + * Render the column in a column and condition phrase - typically in a WHERE clause. + * + *

By default, the column will be rendered as the column alias if it exists, or the column name. + * This can be complicated if the column has a table qualifier, or if the "column" is a function or + * part of a CASE expression. Columns know how to render themselves, so we just call their "render" + * methods. + * + * @param renderingContext the current rendering context + * @param leftColumn the column related to this condition in a where clause + * @return the rendered column + */ + default FragmentAndParameters renderLeftColumn(RenderingContext renderingContext, BindableColumn leftColumn) { + return leftColumn.alias() + .map(FragmentAndParameters::fromFragment) + .orElseGet(() -> leftColumn.render(renderingContext)); + } + + /** + * Subclasses can override this to inform the renderer if the condition should not be included + * in the rendered SQL. Typically, conditions will not render if they are empty. + * + * @return true if the condition should render. + */ + default boolean shouldRender(RenderingContext renderingContext) { + return !isEmpty(); + } + + /** + * Subclasses can override this to indicate whether the condition is considered empty. This is primarily used in + * map and filter operations - the map and filter functions will not be applied if the condition is empty. + * + * @return true if the condition is empty. + */ + default boolean isEmpty() { + return false; + } + + /** + * This method will be called during rendering when {@link RenderableCondition#shouldRender(RenderingContext)} + * returns false. + */ + default void renderingSkipped() {} +} diff --git a/src/main/java/org/mybatis/dynamic/sql/SortSpecification.java b/src/main/java/org/mybatis/dynamic/sql/SortSpecification.java index c60a8ab1e..13cfdee55 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SortSpecification.java +++ b/src/main/java/org/mybatis/dynamic/sql/SortSpecification.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,32 +15,30 @@ */ package org.mybatis.dynamic.sql; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + /** * Defines attributes of columns that are necessary for rendering an order by expression. - * - * @author Jeff Butler * + * @author Jeff Butler */ public interface SortSpecification { /** * Returns a new instance of the SortSpecification that should render as descending in an * ORDER BY clause. - * + * * @return new instance of SortSpecification */ SortSpecification descending(); /** - * Return the column alias or column name. - * - * @return the column alias if one has been specified by the user, or else the column name - */ - String aliasOrName(); - - /** - * Return true if the sort order is descending. - * - * @return true if the SortSpcification should render as descending + * Return a fragment rendered for use in an ORDER BY clause. The fragment should include "DESC" if a + * descending order is desired. + * + * @param renderingContext the current rendering context + * @return a rendered fragment and parameters if applicable + * @since 2.0.0 */ - boolean isDescending(); + FragmentAndParameters renderForOrderBy(RenderingContext renderingContext); } diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java index f579b835e..2a8243999 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java +++ b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -17,15 +17,22 @@ import java.util.Arrays; import java.util.Collection; +import java.util.List; +import java.util.Objects; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; import org.mybatis.dynamic.sql.delete.DeleteDSL; import org.mybatis.dynamic.sql.delete.DeleteModel; import org.mybatis.dynamic.sql.insert.BatchInsertDSL; +import org.mybatis.dynamic.sql.insert.GeneralInsertDSL; import org.mybatis.dynamic.sql.insert.InsertDSL; import org.mybatis.dynamic.sql.insert.InsertSelectDSL; import org.mybatis.dynamic.sql.insert.MultiRowInsertDSL; +import org.mybatis.dynamic.sql.select.ColumnSortSpecification; import org.mybatis.dynamic.sql.select.CountDSL; +import org.mybatis.dynamic.sql.select.HavingDSL; +import org.mybatis.dynamic.sql.select.MultiSelectDSL; import org.mybatis.dynamic.sql.select.QueryExpressionDSL.FromGatherer; import org.mybatis.dynamic.sql.select.SelectDSL; import org.mybatis.dynamic.sql.select.SelectModel; @@ -37,16 +44,19 @@ import org.mybatis.dynamic.sql.select.aggregate.Max; import org.mybatis.dynamic.sql.select.aggregate.Min; import org.mybatis.dynamic.sql.select.aggregate.Sum; +import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseDSL; +import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseDSL; import org.mybatis.dynamic.sql.select.function.Add; +import org.mybatis.dynamic.sql.select.function.Cast; +import org.mybatis.dynamic.sql.select.function.Concat; +import org.mybatis.dynamic.sql.select.function.Concatenate; import org.mybatis.dynamic.sql.select.function.Divide; import org.mybatis.dynamic.sql.select.function.Lower; import org.mybatis.dynamic.sql.select.function.Multiply; +import org.mybatis.dynamic.sql.select.function.OperatorFunction; import org.mybatis.dynamic.sql.select.function.Substring; import org.mybatis.dynamic.sql.select.function.Subtract; import org.mybatis.dynamic.sql.select.function.Upper; -import org.mybatis.dynamic.sql.select.join.EqualTo; -import org.mybatis.dynamic.sql.select.join.JoinCondition; -import org.mybatis.dynamic.sql.select.join.JoinCriterion; import org.mybatis.dynamic.sql.update.UpdateDSL; import org.mybatis.dynamic.sql.update.UpdateModel; import org.mybatis.dynamic.sql.util.Buildable; @@ -103,203 +113,519 @@ public interface SqlBuilder { // statements + + /** + * Renders as select count(distinct column) from table... + * + * @param column + * the column to count + * + * @return the next step in the DSL + */ + static CountDSL.FromGatherer countDistinctColumn(BasicColumn column) { + return CountDSL.countDistinct(column); + } + + /** + * Renders as select count(column) from table... + * + * @param column + * the column to count + * + * @return the next step in the DSL + */ + static CountDSL.FromGatherer countColumn(BasicColumn column) { + return CountDSL.count(column); + } + + /** + * Renders as select count(*) from table... + * + * @param table + * the table to count + * + * @return the next step in the DSL + */ static CountDSL countFrom(SqlTable table) { return CountDSL.countFrom(table); } - + static DeleteDSL deleteFrom(SqlTable table) { return DeleteDSL.deleteFrom(table); } - static InsertDSL.IntoGatherer insert(T record) { - return InsertDSL.insert(record); + static DeleteDSL deleteFrom(SqlTable table, String tableAlias) { + return DeleteDSL.deleteFrom(table, tableAlias); + } + + static InsertDSL.IntoGatherer insert(T row) { + return InsertDSL.insert(row); } - + + /** + * Insert a Batch of records. The model object is structured to support bulk inserts with JDBC batch support. + * + * @param records + * records to insert + * @param + * the type of record to insert + * + * @return the next step in the DSL + */ @SafeVarargs - static BatchInsertDSL.IntoGatherer insert(T...records) { + static BatchInsertDSL.IntoGatherer insertBatch(T... records) { return BatchInsertDSL.insert(records); } - - static BatchInsertDSL.IntoGatherer insert(Collection records) { + + /** + * Insert a Batch of records. The model object is structured to support bulk inserts with JDBC batch support. + * + * @param records + * records to insert + * @param + * the type of record to insert + * + * @return the next step in the DSL + */ + static BatchInsertDSL.IntoGatherer insertBatch(Collection records) { return BatchInsertDSL.insert(records); } - + + /** + * Insert multiple records in a single statement. The model object is structured as a single insert statement with + * multiple values clauses. This statement is suitable for use with a small number of records. It is not suitable + * for large bulk inserts as it is possible to exceed the limit of parameter markers in a prepared statement. + * + *

For large bulk inserts, see {@link SqlBuilder#insertBatch(Object[])} + * @param records + * records to insert + * @param + * the type of record to insert + * + * @return the next step in the DSL + */ @SafeVarargs - static MultiRowInsertDSL.IntoGatherer insertMultiple(T...records) { + static MultiRowInsertDSL.IntoGatherer insertMultiple(T... records) { return MultiRowInsertDSL.insert(records); } - + + /** + * Insert multiple records in a single statement. The model object is structured as a single insert statement with + * multiple values clauses. This statement is suitable for use with a small number of records. It is not suitable + * for large bulk inserts as it is possible to exceed the limit of parameter markers in a prepared statement. + * + *

For large bulk inserts, see {@link SqlBuilder#insertBatch(Collection)} + * @param records + * records to insert + * @param + * the type of record to insert + * + * @return the next step in the DSL + */ static MultiRowInsertDSL.IntoGatherer insertMultiple(Collection records) { return MultiRowInsertDSL.insert(records); } - - static InsertSelectDSL.InsertColumnGatherer insertInto(SqlTable table) { - return InsertSelectDSL.insertInto(table); + + static InsertIntoNextStep insertInto(SqlTable table) { + return new InsertIntoNextStep(table); } - - static FromGatherer select(BasicColumn...selectList) { + + static FromGatherer select(BasicColumn... selectList) { return SelectDSL.select(selectList); } - - static FromGatherer select(Collection selectList) { + + static FromGatherer select(Collection selectList) { return SelectDSL.select(selectList); } - - static FromGatherer selectDistinct(BasicColumn...selectList) { + + static FromGatherer selectDistinct(BasicColumn... selectList) { return SelectDSL.selectDistinct(selectList); } - - static FromGatherer selectDistinct(Collection selectList) { + + static FromGatherer selectDistinct(Collection selectList) { return SelectDSL.selectDistinct(selectList); } - + + static MultiSelectDSL multiSelect(Buildable selectModelBuilder) { + return new MultiSelectDSL(selectModelBuilder); + } + static UpdateDSL update(SqlTable table) { return UpdateDSL.update(table); } - static WhereDSL where() { - return WhereDSL.where(); + static UpdateDSL update(SqlTable table, String tableAlias) { + return UpdateDSL.update(table, tableAlias); } - - static WhereDSL where(BindableColumn column, VisitableCondition condition) { - return WhereDSL.where().where(column, condition); + + static WhereDSL.StandaloneWhereFinisher where() { + return new WhereDSL().where(); } - - static WhereDSL where(BindableColumn column, VisitableCondition condition, - SqlCriterion... subCriteria) { - return WhereDSL.where().where(column, condition, subCriteria); + + static WhereDSL.StandaloneWhereFinisher where(BindableColumn column, RenderableCondition condition, + AndOrCriteriaGroup... subCriteria) { + return new WhereDSL().where(column, condition, subCriteria); + } + + static WhereDSL.StandaloneWhereFinisher where(SqlCriterion initialCriterion, AndOrCriteriaGroup... subCriteria) { + return new WhereDSL().where(initialCriterion, subCriteria); } - + + static WhereDSL.StandaloneWhereFinisher where(ExistsPredicate existsPredicate, AndOrCriteriaGroup... subCriteria) { + return new WhereDSL().where(existsPredicate, subCriteria); + } + + static HavingDSL.StandaloneHavingFinisher having(BindableColumn column, RenderableCondition condition, + AndOrCriteriaGroup... subCriteria) { + return new HavingDSL().having(column, condition, subCriteria); + } + + static HavingDSL.StandaloneHavingFinisher having(SqlCriterion initialCriterion, AndOrCriteriaGroup... subCriteria) { + return new HavingDSL().having(initialCriterion, subCriteria); + } + // where condition connectors - static SqlCriterion or(BindableColumn column, VisitableCondition condition) { - return SqlCriterion.withColumn(column) + static CriteriaGroup group(BindableColumn column, RenderableCondition condition, + AndOrCriteriaGroup... subCriteria) { + return group(column, condition, Arrays.asList(subCriteria)); + } + + static CriteriaGroup group(BindableColumn column, RenderableCondition condition, + List subCriteria) { + return new CriteriaGroup.Builder() + .withInitialCriterion(new ColumnAndConditionCriterion.Builder().withColumn(column) + .withCondition(condition).build()) + .withSubCriteria(subCriteria) + .build(); + } + + static CriteriaGroup group(ExistsPredicate existsPredicate, AndOrCriteriaGroup... subCriteria) { + return group(existsPredicate, Arrays.asList(subCriteria)); + } + + static CriteriaGroup group(ExistsPredicate existsPredicate, List subCriteria) { + return new CriteriaGroup.Builder() + .withInitialCriterion(new ExistsCriterion.Builder() + .withExistsPredicate(existsPredicate).build()) + .withSubCriteria(subCriteria) + .build(); + } + + static CriteriaGroup group(SqlCriterion initialCriterion, AndOrCriteriaGroup... subCriteria) { + return group(initialCriterion, Arrays.asList(subCriteria)); + } + + static CriteriaGroup group(SqlCriterion initialCriterion, List subCriteria) { + return new CriteriaGroup.Builder() + .withInitialCriterion(initialCriterion) + .withSubCriteria(subCriteria) + .build(); + } + + static CriteriaGroup group(List subCriteria) { + return new CriteriaGroup.Builder() + .withSubCriteria(subCriteria) + .build(); + } + + static NotCriterion not(BindableColumn column, RenderableCondition condition, + AndOrCriteriaGroup... subCriteria) { + return not(column, condition, Arrays.asList(subCriteria)); + } + + static NotCriterion not(BindableColumn column, RenderableCondition condition, + List subCriteria) { + return new NotCriterion.Builder() + .withInitialCriterion(new ColumnAndConditionCriterion.Builder().withColumn(column) + .withCondition(condition).build()) + .withSubCriteria(subCriteria) + .build(); + } + + static NotCriterion not(ExistsPredicate existsPredicate, AndOrCriteriaGroup... subCriteria) { + return not(existsPredicate, Arrays.asList(subCriteria)); + } + + static NotCriterion not(ExistsPredicate existsPredicate, List subCriteria) { + return new NotCriterion.Builder() + .withInitialCriterion(new ExistsCriterion.Builder() + .withExistsPredicate(existsPredicate).build()) + .withSubCriteria(subCriteria) + .build(); + } + + static NotCriterion not(SqlCriterion initialCriterion, AndOrCriteriaGroup... subCriteria) { + return not(initialCriterion, Arrays.asList(subCriteria)); + } + + static NotCriterion not(SqlCriterion initialCriterion, List subCriteria) { + return new NotCriterion.Builder() + .withInitialCriterion(initialCriterion) + .withSubCriteria(subCriteria) + .build(); + } + + static NotCriterion not(List subCriteria) { + return new NotCriterion.Builder() + .withSubCriteria(subCriteria) + .build(); + } + + static AndOrCriteriaGroup or(BindableColumn column, RenderableCondition condition, + AndOrCriteriaGroup... subCriteria) { + return new AndOrCriteriaGroup.Builder() + .withInitialCriterion(ColumnAndConditionCriterion.withColumn(column) + .withCondition(condition) + .build()) .withConnector("or") //$NON-NLS-1$ - .withCondition(condition) + .withSubCriteria(Arrays.asList(subCriteria)) + .build(); + } + + static AndOrCriteriaGroup or(ExistsPredicate existsPredicate, AndOrCriteriaGroup... subCriteria) { + return new AndOrCriteriaGroup.Builder() + .withInitialCriterion(new ExistsCriterion.Builder() + .withExistsPredicate(existsPredicate).build()) + .withConnector("or") //$NON-NLS-1$ + .withSubCriteria(Arrays.asList(subCriteria)) .build(); } - static SqlCriterion or(BindableColumn column, VisitableCondition condition, - SqlCriterion...subCriteria) { - return SqlCriterion.withColumn(column) + static AndOrCriteriaGroup or(SqlCriterion initialCriterion, AndOrCriteriaGroup... subCriteria) { + return new AndOrCriteriaGroup.Builder() .withConnector("or") //$NON-NLS-1$ - .withCondition(condition) + .withInitialCriterion(initialCriterion) .withSubCriteria(Arrays.asList(subCriteria)) .build(); } - static SqlCriterion and(BindableColumn column, VisitableCondition condition) { - return SqlCriterion.withColumn(column) + static AndOrCriteriaGroup or(List subCriteria) { + return new AndOrCriteriaGroup.Builder() + .withConnector("or") //$NON-NLS-1$ + .withSubCriteria(subCriteria) + .build(); + } + + static AndOrCriteriaGroup and(BindableColumn column, RenderableCondition condition, + AndOrCriteriaGroup... subCriteria) { + return new AndOrCriteriaGroup.Builder() + .withInitialCriterion(ColumnAndConditionCriterion.withColumn(column) + .withCondition(condition) + .build()) .withConnector("and") //$NON-NLS-1$ - .withCondition(condition) + .withSubCriteria(Arrays.asList(subCriteria)) .build(); } - static SqlCriterion and(BindableColumn column, VisitableCondition condition, - SqlCriterion...subCriteria) { - return SqlCriterion.withColumn(column) + static AndOrCriteriaGroup and(ExistsPredicate existsPredicate, AndOrCriteriaGroup... subCriteria) { + return new AndOrCriteriaGroup.Builder() + .withInitialCriterion(new ExistsCriterion.Builder() + .withExistsPredicate(existsPredicate).build()) .withConnector("and") //$NON-NLS-1$ - .withCondition(condition) .withSubCriteria(Arrays.asList(subCriteria)) .build(); } - // join support - static JoinCriterion and(BasicColumn joinColumn, JoinCondition joinCondition) { - return new JoinCriterion.Builder() + static AndOrCriteriaGroup and(SqlCriterion initialCriterion, AndOrCriteriaGroup... subCriteria) { + return new AndOrCriteriaGroup.Builder() .withConnector("and") //$NON-NLS-1$ - .withJoinColumn(joinColumn) - .withJoinCondition(joinCondition) + .withInitialCriterion(initialCriterion) + .withSubCriteria(Arrays.asList(subCriteria)) .build(); } - - static JoinCriterion on(BasicColumn joinColumn, JoinCondition joinCondition) { - return new JoinCriterion.Builder() - .withConnector("on") //$NON-NLS-1$ - .withJoinColumn(joinColumn) - .withJoinCondition(joinCondition) + + static AndOrCriteriaGroup and(List subCriteria) { + return new AndOrCriteriaGroup.Builder() + .withConnector("and") //$NON-NLS-1$ + .withSubCriteria(subCriteria) + .build(); + } + + // join support + static ColumnAndConditionCriterion on(BindableColumn joinColumn, RenderableCondition joinCondition) { + return ColumnAndConditionCriterion.withColumn(joinColumn) + .withCondition(joinCondition) .build(); } - - static EqualTo equalTo(BasicColumn column) { - return new EqualTo(column); + + /** + * Starting in version 2.0.0, this function is a synonym for {@link SqlBuilder#isEqualTo(BasicColumn)}. + * + * @param column the column + * @param the column type + * @return an IsEqualToColumn condition + * @deprecated since 2.0.0. Please replace with isEqualTo(column) + */ + @Deprecated(since = "2.0.0", forRemoval = true) + static IsEqualToColumn equalTo(BindableColumn column) { + return isEqualTo(column); + } + + /** + * Starting in version 2.0.0, this function is a synonym for {@link SqlBuilder#isEqualTo(Object)}. + * + * @param value the value + * @param the column type + * @return an IsEqualTo condition + * @deprecated since 2.0.0. Please replace with isEqualTo(value) + */ + @Deprecated(since = "2.0.0", forRemoval = true) + static IsEqualTo equalTo(T value) { + return isEqualTo(value); + } + + // case expressions + @SuppressWarnings("java:S100") + static SimpleCaseDSL case_(BindableColumn column) { + return SimpleCaseDSL.simpleCase(column); + } + + @SuppressWarnings("java:S100") + static SearchedCaseDSL case_() { + return SearchedCaseDSL.searchedCase(); } // aggregate support static CountAll count() { return new CountAll(); } - + static Count count(BasicColumn column) { return Count.of(column); } - + static CountDistinct countDistinct(BasicColumn column) { return CountDistinct.of(column); } - - static Max max(BasicColumn column) { + + static SubQueryColumn subQuery(Buildable subQuery) { + return SubQueryColumn.of(subQuery.build()); + } + + static Max max(BindableColumn column) { return Max.of(column); } - - static Min min(BasicColumn column) { + + static Min min(BindableColumn column) { return Min.of(column); } - static Avg avg(BasicColumn column) { + static Avg avg(BindableColumn column) { return Avg.of(column); } - static Sum sum(BasicColumn column) { + static Sum sum(BindableColumn column) { return Sum.of(column); } + static Sum sum(BasicColumn column) { + return Sum.of(column); + } + + static Sum sum(BindableColumn column, RenderableCondition condition) { + return Sum.of(column, condition); + } + // constants - static Constant constant(String constant) { + static Constant constant(String constant) { return Constant.of(constant); } - + static StringConstant stringConstant(String constant) { return StringConstant.of(constant); } - + + static BoundValue value(T value) { + return BoundValue.of(value); + } + // functions - @SafeVarargs - static Add add(BindableColumn firstColumn, BasicColumn secondColumn, + static Add add(BindableColumn firstColumn, BasicColumn secondColumn, BasicColumn... subsequentColumns) { - return Add.of(firstColumn, secondColumn, Arrays.asList(subsequentColumns)); + return Add.of(firstColumn, secondColumn, subsequentColumns); } - - @SafeVarargs - static Divide divide(BindableColumn firstColumn, BasicColumn secondColumn, + + static Divide divide(BindableColumn firstColumn, BasicColumn secondColumn, BasicColumn... subsequentColumns) { - return Divide.of(firstColumn, secondColumn, Arrays.asList(subsequentColumns)); + return Divide.of(firstColumn, secondColumn, subsequentColumns); } - - @SafeVarargs - static Multiply multiply(BindableColumn firstColumn, BasicColumn secondColumn, + + static Multiply multiply(BindableColumn firstColumn, BasicColumn secondColumn, BasicColumn... subsequentColumns) { - return Multiply.of(firstColumn, secondColumn, Arrays.asList(subsequentColumns)); + return Multiply.of(firstColumn, secondColumn, subsequentColumns); } - - @SafeVarargs - static Subtract subtract(BindableColumn firstColumn, BasicColumn secondColumn, + + static Subtract subtract(BindableColumn firstColumn, BasicColumn secondColumn, + BasicColumn... subsequentColumns) { + return Subtract.of(firstColumn, secondColumn, subsequentColumns); + } + + static CastFinisher cast(String value) { + return cast(stringConstant(value)); + } + + static CastFinisher cast(Double value) { + return cast(constant(value.toString())); + } + + static CastFinisher cast(BasicColumn column) { + return new CastFinisher(column); + } + + /** + * Concatenate function that renders as "(x || y || z)". This will not work on some + * databases like MySql. In that case, use {@link SqlBuilder#concat(BindableColumn, BasicColumn...)} + * + * @param firstColumn first column + * @param secondColumn second column + * @param subsequentColumns subsequent columns + * @param type of column + * @return a Concatenate instance + */ + static Concatenate concatenate(BindableColumn firstColumn, BasicColumn secondColumn, BasicColumn... subsequentColumns) { - return Subtract.of(firstColumn, secondColumn, Arrays.asList(subsequentColumns)); + return Concatenate.concatenate(firstColumn, secondColumn, subsequentColumns); } - - static Lower lower(BindableColumn column) { + + /** + * Concatenate function that renders as "concat(x, y, z)". This version works on more databases + * than {@link SqlBuilder#concatenate(BindableColumn, BasicColumn, BasicColumn...)} + * + * @param firstColumn first column + * @param subsequentColumns subsequent columns + * @param type of column + * @return a Concat instance + */ + static Concat concat(BindableColumn firstColumn, BasicColumn... subsequentColumns) { + return Concat.concat(firstColumn, subsequentColumns); + } + + static OperatorFunction applyOperator(String operator, BindableColumn firstColumn, + BasicColumn secondColumn, BasicColumn... subsequentColumns) { + return OperatorFunction.of(operator, firstColumn, secondColumn, subsequentColumns); + } + + static Lower lower(BindableColumn column) { return Lower.of(column); } - - static Substring substring(BindableColumn column, int offset, int length) { + + static Substring substring(BindableColumn column, int offset, int length) { return Substring.of(column, offset, length); } - - static Upper upper(BindableColumn column) { + + static Upper upper(BindableColumn column) { return Upper.of(column); } - + // conditions for all data types + static ExistsPredicate exists(Buildable selectModelBuilder) { + return ExistsPredicate.exists(selectModelBuilder); + } + + static ExistsPredicate notExists(Buildable selectModelBuilder) { + return ExistsPredicate.notExists(selectModelBuilder); + } + static IsNull isNull() { return new IsNull<>(); } @@ -309,35 +635,35 @@ static IsNotNull isNotNull() { } static IsEqualTo isEqualTo(T value) { - return isEqualTo(() -> value); + return IsEqualTo.of(value); } static IsEqualTo isEqualTo(Supplier valueSupplier) { - return IsEqualTo.of(valueSupplier); + return isEqualTo(valueSupplier.get()); } static IsEqualToWithSubselect isEqualTo(Buildable selectModelBuilder) { return IsEqualToWithSubselect.of(selectModelBuilder); } - + static IsEqualToColumn isEqualTo(BasicColumn column) { return IsEqualToColumn.of(column); } - static IsEqualToWhenPresent isEqualToWhenPresent(T value) { - return isEqualToWhenPresent(() -> value); + static IsEqualToWhenPresent isEqualToWhenPresent(@Nullable T value) { + return IsEqualToWhenPresent.of(value); } - static IsEqualToWhenPresent isEqualToWhenPresent(Supplier valueSupplier) { - return IsEqualToWhenPresent.of(valueSupplier); + static IsEqualToWhenPresent isEqualToWhenPresent(Supplier<@Nullable T> valueSupplier) { + return isEqualToWhenPresent(valueSupplier.get()); } - + static IsNotEqualTo isNotEqualTo(T value) { - return isNotEqualTo(() -> value); + return IsNotEqualTo.of(value); } static IsNotEqualTo isNotEqualTo(Supplier valueSupplier) { - return IsNotEqualTo.of(valueSupplier); + return isNotEqualTo(valueSupplier.get()); } static IsNotEqualToWithSubselect isNotEqualTo(Buildable selectModelBuilder) { @@ -348,218 +674,219 @@ static IsNotEqualToColumn isNotEqualTo(BasicColumn column) { return IsNotEqualToColumn.of(column); } - static IsNotEqualToWhenPresent isNotEqualToWhenPresent(T value) { - return isNotEqualToWhenPresent(() -> value); + static IsNotEqualToWhenPresent isNotEqualToWhenPresent(@Nullable T value) { + return IsNotEqualToWhenPresent.of(value); } - static IsNotEqualToWhenPresent isNotEqualToWhenPresent(Supplier valueSupplier) { - return IsNotEqualToWhenPresent.of(valueSupplier); + static IsNotEqualToWhenPresent isNotEqualToWhenPresent(Supplier<@Nullable T> valueSupplier) { + return isNotEqualToWhenPresent(valueSupplier.get()); } - + static IsGreaterThan isGreaterThan(T value) { - return isGreaterThan(() -> value); + return IsGreaterThan.of(value); } - + static IsGreaterThan isGreaterThan(Supplier valueSupplier) { - return IsGreaterThan.of(valueSupplier); + return isGreaterThan(valueSupplier.get()); } - + static IsGreaterThanWithSubselect isGreaterThan(Buildable selectModelBuilder) { return IsGreaterThanWithSubselect.of(selectModelBuilder); } - + static IsGreaterThanColumn isGreaterThan(BasicColumn column) { return IsGreaterThanColumn.of(column); } - static IsGreaterThanWhenPresent isGreaterThanWhenPresent(T value) { - return isGreaterThanWhenPresent(() -> value); + static IsGreaterThanWhenPresent isGreaterThanWhenPresent(@Nullable T value) { + return IsGreaterThanWhenPresent.of(value); } - - static IsGreaterThanWhenPresent isGreaterThanWhenPresent(Supplier valueSupplier) { - return IsGreaterThanWhenPresent.of(valueSupplier); + + static IsGreaterThanWhenPresent isGreaterThanWhenPresent(Supplier<@Nullable T> valueSupplier) { + return isGreaterThanWhenPresent(valueSupplier.get()); } - + static IsGreaterThanOrEqualTo isGreaterThanOrEqualTo(T value) { - return isGreaterThanOrEqualTo(() -> value); + return IsGreaterThanOrEqualTo.of(value); } - + static IsGreaterThanOrEqualTo isGreaterThanOrEqualTo(Supplier valueSupplier) { - return IsGreaterThanOrEqualTo.of(valueSupplier); + return isGreaterThanOrEqualTo(valueSupplier.get()); } - + static IsGreaterThanOrEqualToWithSubselect isGreaterThanOrEqualTo( Buildable selectModelBuilder) { return IsGreaterThanOrEqualToWithSubselect.of(selectModelBuilder); } - + static IsGreaterThanOrEqualToColumn isGreaterThanOrEqualTo(BasicColumn column) { return IsGreaterThanOrEqualToColumn.of(column); } - static IsGreaterThanOrEqualToWhenPresent isGreaterThanOrEqualToWhenPresent(T value) { - return isGreaterThanOrEqualToWhenPresent(() -> value); + static IsGreaterThanOrEqualToWhenPresent isGreaterThanOrEqualToWhenPresent(@Nullable T value) { + return IsGreaterThanOrEqualToWhenPresent.of(value); } - - static IsGreaterThanOrEqualToWhenPresent isGreaterThanOrEqualToWhenPresent(Supplier valueSupplier) { - return IsGreaterThanOrEqualToWhenPresent.of(valueSupplier); + + static IsGreaterThanOrEqualToWhenPresent isGreaterThanOrEqualToWhenPresent( + Supplier<@Nullable T> valueSupplier) { + return isGreaterThanOrEqualToWhenPresent(valueSupplier.get()); } - + static IsLessThan isLessThan(T value) { - return isLessThan(() -> value); + return IsLessThan.of(value); } - + static IsLessThan isLessThan(Supplier valueSupplier) { - return IsLessThan.of(valueSupplier); + return isLessThan(valueSupplier.get()); } - + static IsLessThanWithSubselect isLessThan(Buildable selectModelBuilder) { return IsLessThanWithSubselect.of(selectModelBuilder); } - + static IsLessThanColumn isLessThan(BasicColumn column) { return IsLessThanColumn.of(column); } - static IsLessThanWhenPresent isLessThanWhenPresent(T value) { - return isLessThanWhenPresent(() -> value); + static IsLessThanWhenPresent isLessThanWhenPresent(@Nullable T value) { + return IsLessThanWhenPresent.of(value); } - - static IsLessThanWhenPresent isLessThanWhenPresent(Supplier valueSupplier) { - return IsLessThanWhenPresent.of(valueSupplier); + + static IsLessThanWhenPresent isLessThanWhenPresent(Supplier<@Nullable T> valueSupplier) { + return isLessThanWhenPresent(valueSupplier.get()); } - + static IsLessThanOrEqualTo isLessThanOrEqualTo(T value) { - return isLessThanOrEqualTo(() -> value); + return IsLessThanOrEqualTo.of(value); } - + static IsLessThanOrEqualTo isLessThanOrEqualTo(Supplier valueSupplier) { - return IsLessThanOrEqualTo.of(valueSupplier); + return isLessThanOrEqualTo(valueSupplier.get()); } - + static IsLessThanOrEqualToWithSubselect isLessThanOrEqualTo(Buildable selectModelBuilder) { return IsLessThanOrEqualToWithSubselect.of(selectModelBuilder); } - + static IsLessThanOrEqualToColumn isLessThanOrEqualTo(BasicColumn column) { return IsLessThanOrEqualToColumn.of(column); } - static IsLessThanOrEqualToWhenPresent isLessThanOrEqualToWhenPresent(T value) { - return isLessThanOrEqualToWhenPresent(() -> value); + static IsLessThanOrEqualToWhenPresent isLessThanOrEqualToWhenPresent(@Nullable T value) { + return IsLessThanOrEqualToWhenPresent.of(value); } - - static IsLessThanOrEqualToWhenPresent isLessThanOrEqualToWhenPresent(Supplier valueSupplier) { - return IsLessThanOrEqualToWhenPresent.of(valueSupplier); + + static IsLessThanOrEqualToWhenPresent isLessThanOrEqualToWhenPresent(Supplier<@Nullable T> valueSupplier) { + return isLessThanOrEqualToWhenPresent(valueSupplier.get()); } - + @SafeVarargs - static IsIn isIn(T...values) { - return isIn(Arrays.asList(values)); + static IsIn isIn(T... values) { + return IsIn.of(values); } static IsIn isIn(Collection values) { return IsIn.of(values); } - + static IsInWithSubselect isIn(Buildable selectModelBuilder) { return IsInWithSubselect.of(selectModelBuilder); } @SafeVarargs - static IsInWhenPresent isInWhenPresent(T...values) { - return isInWhenPresent(Arrays.asList(values)); + static IsInWhenPresent isInWhenPresent(@Nullable T... values) { + return IsInWhenPresent.of(values); } - static IsInWhenPresent isInWhenPresent(Collection values) { + static IsInWhenPresent isInWhenPresent(@Nullable Collection<@Nullable T> values) { return IsInWhenPresent.of(values); } - + @SafeVarargs - static IsNotIn isNotIn(T...values) { - return isNotIn(Arrays.asList(values)); + static IsNotIn isNotIn(T... values) { + return IsNotIn.of(values); } - + static IsNotIn isNotIn(Collection values) { return IsNotIn.of(values); } - + static IsNotInWithSubselect isNotIn(Buildable selectModelBuilder) { return IsNotInWithSubselect.of(selectModelBuilder); } @SafeVarargs - static IsNotInWhenPresent isNotInWhenPresent(T...values) { - return isNotInWhenPresent(Arrays.asList(values)); + static IsNotInWhenPresent isNotInWhenPresent(@Nullable T... values) { + return IsNotInWhenPresent.of(values); } - static IsNotInWhenPresent isNotInWhenPresent(Collection values) { + static IsNotInWhenPresent isNotInWhenPresent(@Nullable Collection<@Nullable T> values) { return IsNotInWhenPresent.of(values); } - + static IsBetween.Builder isBetween(T value1) { - return isBetween(() -> value1); + return IsBetween.isBetween(value1); } - + static IsBetween.Builder isBetween(Supplier valueSupplier1) { - return IsBetween.isBetween(valueSupplier1); + return isBetween(valueSupplier1.get()); } - - static IsBetweenWhenPresent.Builder isBetweenWhenPresent(T value1) { - return isBetweenWhenPresent(() -> value1); + + static IsBetweenWhenPresent.Builder isBetweenWhenPresent(@Nullable T value1) { + return IsBetweenWhenPresent.isBetweenWhenPresent(value1); } - - static IsBetweenWhenPresent.Builder isBetweenWhenPresent(Supplier valueSupplier1) { - return IsBetweenWhenPresent.isBetweenWhenPresent(valueSupplier1); + + static IsBetweenWhenPresent.Builder isBetweenWhenPresent(Supplier<@Nullable T> valueSupplier1) { + return isBetweenWhenPresent(valueSupplier1.get()); } - + static IsNotBetween.Builder isNotBetween(T value1) { - return isNotBetween(() -> value1); + return IsNotBetween.isNotBetween(value1); } - + static IsNotBetween.Builder isNotBetween(Supplier valueSupplier1) { - return IsNotBetween.isNotBetween(valueSupplier1); + return isNotBetween(valueSupplier1.get()); } - static IsNotBetweenWhenPresent.Builder isNotBetweenWhenPresent(T value1) { - return isNotBetweenWhenPresent(() -> value1); + static IsNotBetweenWhenPresent.Builder isNotBetweenWhenPresent(@Nullable T value1) { + return IsNotBetweenWhenPresent.isNotBetweenWhenPresent(value1); } - - static IsNotBetweenWhenPresent.Builder isNotBetweenWhenPresent(Supplier valueSupplier1) { - return IsNotBetweenWhenPresent.isNotBetweenWhenPresent(valueSupplier1); + + static IsNotBetweenWhenPresent.Builder isNotBetweenWhenPresent(Supplier<@Nullable T> valueSupplier1) { + return isNotBetweenWhenPresent(valueSupplier1.get()); } - + // for string columns, but generic for columns with type handlers static IsLike isLike(T value) { - return isLike(() -> value); + return IsLike.of(value); } - + static IsLike isLike(Supplier valueSupplier) { - return IsLike.of(valueSupplier); + return isLike(valueSupplier.get()); } - - static IsLikeWhenPresent isLikeWhenPresent(T value) { - return isLikeWhenPresent(() -> value); + + static IsLikeWhenPresent isLikeWhenPresent(@Nullable T value) { + return IsLikeWhenPresent.of(value); } - - static IsLikeWhenPresent isLikeWhenPresent(Supplier valueSupplier) { - return IsLikeWhenPresent.of(valueSupplier); + + static IsLikeWhenPresent isLikeWhenPresent(Supplier<@Nullable T> valueSupplier) { + return isLikeWhenPresent(valueSupplier.get()); } - + static IsNotLike isNotLike(T value) { - return isNotLike(() -> value); + return IsNotLike.of(value); } - + static IsNotLike isNotLike(Supplier valueSupplier) { - return IsNotLike.of(valueSupplier); + return isNotLike(valueSupplier.get()); } - - static IsNotLikeWhenPresent isNotLikeWhenPresent(T value) { - return isNotLikeWhenPresent(() -> value); + + static IsNotLikeWhenPresent isNotLikeWhenPresent(@Nullable T value) { + return IsNotLikeWhenPresent.of(value); } - - static IsNotLikeWhenPresent isNotLikeWhenPresent(Supplier valueSupplier) { - return IsNotLikeWhenPresent.of(valueSupplier); + + static IsNotLikeWhenPresent isNotLikeWhenPresent(Supplier<@Nullable T> valueSupplier) { + return isNotLikeWhenPresent(valueSupplier.get()); } // shortcuts for booleans @@ -572,72 +899,147 @@ static IsEqualTo isFalse() { } // conditions for strings only - static IsLikeCaseInsensitive isLikeCaseInsensitive(String value) { - return isLikeCaseInsensitive(() -> value); + static IsLikeCaseInsensitive isLikeCaseInsensitive(String value) { + return IsLikeCaseInsensitive.of(value); } - - static IsLikeCaseInsensitive isLikeCaseInsensitive(Supplier valueSupplier) { - return IsLikeCaseInsensitive.of(valueSupplier); + + static IsLikeCaseInsensitive isLikeCaseInsensitive(Supplier valueSupplier) { + return isLikeCaseInsensitive(valueSupplier.get()); } - - static IsLikeCaseInsensitiveWhenPresent isLikeCaseInsensitiveWhenPresent(String value) { - return isLikeCaseInsensitiveWhenPresent(() -> value); + + static IsLikeCaseInsensitiveWhenPresent isLikeCaseInsensitiveWhenPresent(@Nullable String value) { + return IsLikeCaseInsensitiveWhenPresent.of(value); } - - static IsLikeCaseInsensitiveWhenPresent isLikeCaseInsensitiveWhenPresent(Supplier valueSupplier) { - return IsLikeCaseInsensitiveWhenPresent.of(valueSupplier); + + static IsLikeCaseInsensitiveWhenPresent isLikeCaseInsensitiveWhenPresent( + Supplier<@Nullable String> valueSupplier) { + return isLikeCaseInsensitiveWhenPresent(valueSupplier.get()); } - - static IsNotLikeCaseInsensitive isNotLikeCaseInsensitive(String value) { - return isNotLikeCaseInsensitive(() -> value); + + static IsNotLikeCaseInsensitive isNotLikeCaseInsensitive(String value) { + return IsNotLikeCaseInsensitive.of(value); } - static IsNotLikeCaseInsensitive isNotLikeCaseInsensitive(Supplier valueSupplier) { - return IsNotLikeCaseInsensitive.of(valueSupplier); + static IsNotLikeCaseInsensitive isNotLikeCaseInsensitive(Supplier valueSupplier) { + return isNotLikeCaseInsensitive(valueSupplier.get()); } - static IsNotLikeCaseInsensitiveWhenPresent isNotLikeCaseInsensitiveWhenPresent(String value) { - return isNotLikeCaseInsensitiveWhenPresent(() -> value); + static IsNotLikeCaseInsensitiveWhenPresent isNotLikeCaseInsensitiveWhenPresent(@Nullable String value) { + return IsNotLikeCaseInsensitiveWhenPresent.of(value); } - - static IsNotLikeCaseInsensitiveWhenPresent isNotLikeCaseInsensitiveWhenPresent(Supplier valueSupplier) { - return IsNotLikeCaseInsensitiveWhenPresent.of(valueSupplier); + + static IsNotLikeCaseInsensitiveWhenPresent isNotLikeCaseInsensitiveWhenPresent( + Supplier<@Nullable String> valueSupplier) { + return isNotLikeCaseInsensitiveWhenPresent(valueSupplier.get()); } - - static IsInCaseInsensitive isInCaseInsensitive(String...values) { - return isInCaseInsensitive(Arrays.asList(values)); + + static IsInCaseInsensitive isInCaseInsensitive(String... values) { + return IsInCaseInsensitive.of(values); } - static IsInCaseInsensitive isInCaseInsensitive(Collection values) { + static IsInCaseInsensitive isInCaseInsensitive(Collection values) { return IsInCaseInsensitive.of(values); } - static IsInCaseInsensitiveWhenPresent isInCaseInsensitiveWhenPresent(String...values) { - return isInCaseInsensitiveWhenPresent(Arrays.asList(values)); + static IsInCaseInsensitiveWhenPresent isInCaseInsensitiveWhenPresent(@Nullable String... values) { + return IsInCaseInsensitiveWhenPresent.of(values); } - static IsInCaseInsensitiveWhenPresent isInCaseInsensitiveWhenPresent(Collection values) { + static IsInCaseInsensitiveWhenPresent isInCaseInsensitiveWhenPresent( + @Nullable Collection<@Nullable String> values) { return IsInCaseInsensitiveWhenPresent.of(values); } - static IsNotInCaseInsensitive isNotInCaseInsensitive(String...values) { - return isNotInCaseInsensitive(Arrays.asList(values)); + static IsNotInCaseInsensitive isNotInCaseInsensitive(String... values) { + return IsNotInCaseInsensitive.of(values); } - - static IsNotInCaseInsensitive isNotInCaseInsensitive(Collection values) { + + static IsNotInCaseInsensitive isNotInCaseInsensitive(Collection values) { return IsNotInCaseInsensitive.of(values); } - - static IsNotInCaseInsensitiveWhenPresent isNotInCaseInsensitiveWhenPresent(String...values) { - return isNotInCaseInsensitiveWhenPresent(Arrays.asList(values)); + + static IsNotInCaseInsensitiveWhenPresent isNotInCaseInsensitiveWhenPresent(@Nullable String... values) { + return IsNotInCaseInsensitiveWhenPresent.of(values); } - static IsNotInCaseInsensitiveWhenPresent isNotInCaseInsensitiveWhenPresent(Collection values) { + static IsNotInCaseInsensitiveWhenPresent isNotInCaseInsensitiveWhenPresent( + @Nullable Collection<@Nullable String> values) { return IsNotInCaseInsensitiveWhenPresent.of(values); } // order by support + + /** + * Creates a sort specification based on a String. This is useful when a column has been + * aliased in the select list. For example: + * + *
+     *     select(foo.as("bar"))
+     *     .from(baz)
+     *     .orderBy(sortColumn("bar"))
+     * 
+ * + * @param name the string to use as a sort specification + * @return a sort specification + */ static SortSpecification sortColumn(String name) { return SimpleSortSpecification.of(name); } + + /** + * Creates a sort specification based on a column and a table alias. This can be useful in a join + * where the desired sort order is based on a column not in the select list. This will likely + * fail in union queries depending on database support. + * + * @param tableAlias the table alias + * @param column the column + * @return a sort specification + */ + static SortSpecification sortColumn(String tableAlias, SqlColumn column) { + return new ColumnSortSpecification(tableAlias, column); + } + + class InsertIntoNextStep { + + private final SqlTable table; + + private InsertIntoNextStep(SqlTable table) { + this.table = Objects.requireNonNull(table); + } + + public InsertSelectDSL withSelectStatement(Buildable selectModelBuilder) { + return InsertSelectDSL.insertInto(table) + .withSelectStatement(selectModelBuilder); + } + + public InsertSelectDSL.SelectGatherer withColumnList(SqlColumn... columns) { + return InsertSelectDSL.insertInto(table) + .withColumnList(columns); + } + + public InsertSelectDSL.SelectGatherer withColumnList(List> columns) { + return InsertSelectDSL.insertInto(table) + .withColumnList(columns); + } + + public GeneralInsertDSL.SetClauseFinisher set(SqlColumn column) { + return GeneralInsertDSL.insertInto(table) + .set(column); + } + } + + class CastFinisher { + private final BasicColumn column; + + public CastFinisher(BasicColumn column) { + this.column = column; + } + + public Cast as(String targetType) { + return new Cast.Builder() + .withColumn(column) + .withTargetType(targetType) + .build(); + } + } } diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlColumn.java b/src/main/java/org/mybatis/dynamic/sql/SqlColumn.java index 5b5061ed3..d8f9dc56e 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SqlColumn.java +++ b/src/main/java/org/mybatis/dynamic/sql/SqlColumn.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -19,37 +19,119 @@ import java.util.Objects; import java.util.Optional; -import org.mybatis.dynamic.sql.render.TableAliasCalculator; +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.render.RenderingStrategy; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.StringUtilities; +/** + * This class represents the definition of a column in a table. + * + *

The class contains many attributes that are helpful for use in MyBatis and Spring runtime + * environments, but the only required attributes are the name of the column and a reference to + * the {@link SqlTable} the column is a part of. + * + *

The class can be extended if you wish to associate additional attributes with a column for your + * own purposes. Extending the class is a bit more challenging than you might expect because you may need to + * handle the covariant types for many methods in {@code SqlColumn}. Additionally, many methods in {@code SqlColumn} + * create new instances of the class in keeping with the library's primary strategy of immutability. You will also + * need to ensure that these methods create instances of your extended class, rather than the base {@code SqlColumn} + * class. We have worked to keep this process as simple as possible. + * + *

Extending the class involves the following activities: + *

    + *
  1. Create a class that extends {@link SqlColumn}
  2. + *
  3. In your extended class, create a static builder class that extends {@link SqlColumn.AbstractBuilder}
  4. + *
  5. Add your desired attributes to the class and the builder
  6. + *
  7. You MUST override the {@link SqlColumn#copyBuilder()} method and return a new instance of + * your builder with all attributes set. In the overridden method you should call the superclass + * {@link SqlColumn#populateBaseBuilder(AbstractBuilder)} method + * to set the attributes from the base {@code SqlColumn}, then populate your extended attributes. During normal + * usage, the library may create additional instances of your class. If you do not override the + * {@link SqlColumn#copyBuilder()} method properly, then your extended attributes will be lost. + *
  8. + *
  9. You MAY override the following methods. These methods are used with regular operations in the library and + * create new instances of the class. However, these methods are not typically chained, so losing the specific + * type may not be a problem. If you want to preserve the type, then you can override these methods + * to specify the covariant return type. See below for usage of the {@link SqlColumn#cast(SqlColumn)} method + * to make it easier to override these methods. + *
      + *
    • {@link SqlColumn#as(String)}
    • + *
    • {@link SqlColumn#asCamelCase()}
    • + *
    • {@link SqlColumn#descending()}
    • + *
    • {@link SqlColumn#qualifiedWith(String)}
    • + *
    + *
  10. + *
  11. You SHOULD override the following methods. These methods can be used to add additional attributes to a + * column by creating a new instance with a specified attribute set. These methods are used during the + * construction of columns. If you do not override these methods, and a user calls them, then the specific type + * will be lost. If you want to preserve the type, then you can override these methods + * to specify the covariant return type. See below for usage of the {@link SqlColumn#cast(SqlColumn)} method + * to make it easier to override these methods. + *
      + *
    • {@link SqlColumn#withJavaProperty(String)}
    • + *
    • {@link SqlColumn#withRenderingStrategy(RenderingStrategy)}
    • + *
    • {@link SqlColumn#withTypeHandler(String)}
    • + *
    • {@link SqlColumn#withJavaType(Class)}
    • + *
    • {@link SqlColumn#withParameterTypeConverter(ParameterTypeConverter)}
    • + *
    + *
  12. + *
+ * + *

For all overridden methods except {@code copyBuilder()}, the process is to call the superclass + * method and cast the result properly. We provide a {@link SqlColumn#cast(SqlColumn)} method to aid with this + * process. For example, overriding the {@code descending} method could look like this: + * + *

+ * {@code
+ * @Override
+ * public MyExtendedColumn descending() {
+ *     return cast(super.descending());
+ * }
+ * }
+ * 
+ * + *

The test code for this library contains an example of a fully executed extension of this class. + * + * @param the Java type associated with the column + */ public class SqlColumn implements BindableColumn, SortSpecification { - - protected String name; - protected SqlTable table; - protected JDBCType jdbcType; - protected boolean isDescending = false; - protected String alias; - protected String typeHandler; - - private SqlColumn(Builder builder) { + + protected final String name; + protected final SqlTable table; + protected final @Nullable JDBCType jdbcType; + protected final String descendingPhrase; + protected final @Nullable String alias; + protected final @Nullable String typeHandler; + protected final @Nullable RenderingStrategy renderingStrategy; + protected final ParameterTypeConverter parameterTypeConverter; + protected final @Nullable String tableQualifier; + protected final @Nullable Class javaType; + protected final @Nullable String javaProperty; + + protected SqlColumn(AbstractBuilder builder) { name = Objects.requireNonNull(builder.name); - jdbcType = builder.jdbcType; table = Objects.requireNonNull(builder.table); + jdbcType = builder.jdbcType; + descendingPhrase = builder.descendingPhrase; + alias = builder.alias; typeHandler = builder.typeHandler; + renderingStrategy = builder.renderingStrategy; + parameterTypeConverter = Objects.requireNonNull(builder.parameterTypeConverter); + tableQualifier = builder.tableQualifier; + javaType = builder.javaType; + javaProperty = builder.javaProperty; } - - protected SqlColumn(SqlColumn sqlColumn) { - name = sqlColumn.name; - table = sqlColumn.table; - jdbcType = sqlColumn.jdbcType; - isDescending = sqlColumn.isDescending; - alias = sqlColumn.alias; - typeHandler = sqlColumn.typeHandler; - } - + public String name() { return name; } - + + public SqlTable table() { + return table; + } + @Override public Optional jdbcType() { return Optional.ofNullable(jdbcType); @@ -59,98 +141,324 @@ public Optional jdbcType() { public Optional alias() { return Optional.ofNullable(alias); } - + @Override public Optional typeHandler() { return Optional.ofNullable(typeHandler); } - + + @Override + public Optional> javaType() { + return Optional.ofNullable(javaType); + } + + public Optional javaProperty() { + return Optional.ofNullable(javaProperty); + } + @Override - public SortSpecification descending() { - SqlColumn column = new SqlColumn<>(this); - column.isDescending = true; - return column; + public @Nullable Object convertParameterType(@Nullable T value) { + return value == null ? null : parameterTypeConverter.convert(value); } - + + /** + * Create a new column instance that will render as descending when used in an order by phrase. + * + * @return a new column instance that will render as descending when used in an order by phrase + */ + @Override + public SqlColumn descending() { + return copyBuilder().withDescendingPhrase(" DESC").build(); //$NON-NLS-1$ + } + + /** + * Create a new column instance with the specified alias that will render as "as alias" in a column list. + * + * @param alias + * the column alias to set + * + * @return a new column instance with the specified alias + */ @Override public SqlColumn as(String alias) { - SqlColumn column = new SqlColumn<>(this); - column.alias = alias; - return column; + return copyBuilder().withAlias(alias).build(); + } + + /** + * Override the calculated table qualifier if there is one. This is useful for sub-queries + * where the calculated table qualifier may not be correct in all cases. + * + * @param tableQualifier the table qualifier to apply to the rendered column name + * @return a new column that will be rendered with the specified table qualifier + */ + public SqlColumn qualifiedWith(String tableQualifier) { + return copyBuilder().withTableQualifier(tableQualifier).build(); + } + + /** + * Set an alias with a camel-cased string based on the column name. This can be useful for queries using + * the {@link org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper} where the columns are placed into + * a map based on the column name returned from the database. + * + *

A camel case string is a mixed case string, and most databases do not support unquoted mixed case strings + * as identifiers. Therefore, the generated alias will be surrounded by double quotes thereby making it a + * quoted identifier. Most databases will respect quoted mixed case identifiers. + * + * @return a new column aliased with a camel case version of the column name + */ + public SqlColumn asCamelCase() { + return copyBuilder() + .withAlias("\"" + StringUtilities.toCamelCase(name) + "\"") //$NON-NLS-1$ //$NON-NLS-2$ + .build(); } - + @Override - public boolean isDescending() { - return isDescending; + public FragmentAndParameters renderForOrderBy(RenderingContext renderingContext) { + return FragmentAndParameters.fromFragment(alias().orElse(name) + descendingPhrase); } - + @Override - public String aliasOrName() { - return alias().orElse(name); + public FragmentAndParameters render(RenderingContext renderingContext) { + if (tableQualifier == null) { + return FragmentAndParameters.fromFragment(renderingContext.aliasedColumnName(this)); + } else { + return FragmentAndParameters.fromFragment(renderingContext.aliasedColumnName(this, tableQualifier)); + } } - + @Override - public String renderWithTableAlias(TableAliasCalculator tableAliasCalculator) { - return tableAliasCalculator.aliasForColumn(table) - .map(this::applyTableAlias) - .orElseGet(this::name); + public Optional renderingStrategy() { + return Optional.ofNullable(renderingStrategy); + } + + /** + * Create a new column instance with the specified type handler. + * + *

This method uses a different type (S). This allows it to be chained with the other + * with* methods. Using new types forces the compiler to delay type inference until the end of a call chain. + * Without this different type (for example, if we used T), the compiler would erase the type after the call + * and method chaining would not work. This is a workaround for Java's lack of reification. + * + * @param typeHandler the type handler to set + * @param the type of the new column (will be the same as T) + * @return a new column instance with the specified type handler + */ + public SqlColumn withTypeHandler(String typeHandler) { + return cast(copyBuilder().withTypeHandler(typeHandler).build()); + } + + /** + * Create a new column instance with the specified rendering strategy. + * + *

This method uses a different type (S). This allows it to be chained with the other + * with* methods. Using new types forces the compiler to delay type inference until the end of a call chain. + * Without this different type (for example, if we used T), the compiler would erase the type after the call + * and method chaining would not work. This is a workaround for Java's lack of reification. + * + * @param renderingStrategy the rendering strategy to set + * @param the type of the new column (will be the same as T) + * @return a new column instance with the specified type handler + */ + public SqlColumn withRenderingStrategy(RenderingStrategy renderingStrategy) { + return cast(copyBuilder().withRenderingStrategy(renderingStrategy).build()); + } + + /** + * Create a new column instance with the specified parameter type converter. + * + *

Parameter type converters are useful with Spring JDBC. Typically, they are not needed for MyBatis. + * + *

This method uses a different type (S). This allows it to be chained with the other + * with* methods. Using new types forces the compiler to delay type inference until the end of a call chain. + * Without this different type (for example, if we used T), the compiler would erase the type after the call + * and method chaining would not work. This is a workaround for Java's lack of reification. + * + * @param parameterTypeConverter the parameter type converter to set + * @param the type of the new column (will be the same as T) + * @return a new column instance with the specified type handler + */ + @SuppressWarnings("unchecked") + public SqlColumn withParameterTypeConverter(ParameterTypeConverter parameterTypeConverter) { + return cast(copyBuilder().withParameterTypeConverter((ParameterTypeConverter) parameterTypeConverter) + .build()); + } + + /** + * Create a new column instance with the specified Java type. + * + *

Specifying a Java type will force rendering of the Java type for MyBatis parameters. This can be useful + * with some MyBatis type handlers. + * + *

This method uses a different type (S). This allows it to be chained with the other + * with* methods. Using new types forces the compiler to delay type inference until the end of a call chain. + * Without this different type (for example, if we used T), the compiler would erase the type after the call + * and method chaining would not work. This is a workaround for Java's lack of reification. + * + * @param javaType the Java type to set + * @param the type of the new column (will be the same as T) + * @return a new column instance with the specified type handler + */ + @SuppressWarnings("unchecked") + public SqlColumn withJavaType(Class javaType) { + return cast(copyBuilder().withJavaType((Class) javaType).build()); } - - public SqlColumn withTypeHandler(String typeHandler) { - SqlColumn column = new SqlColumn<>(this); - column.typeHandler = typeHandler; - return column; + + /** + * Create a new column instance with the specified Java property. + * + *

Specifying a Java property in the column will allow usage of the column as a "mapped column" in record-based + * insert statements. + * + *

This method uses a different type (S). This allows it to be chained with the other + * with* methods. Using new types forces the compiler to delay type inference until the end of a call chain. + * Without this different type (for example, if we used T), the compiler would erase the type after the call + * and method chaining would not work. This is a workaround for Java's lack of reification. + * + * @param javaProperty the Java property to set + * @param the type of the new column (will be the same as T) + * @return a new column instance with the specified type handler + */ + public SqlColumn withJavaProperty(String javaProperty) { + return cast(copyBuilder().withJavaProperty(javaProperty).build()); + } + + /** + * Create a new Builder, then populate all attributes in the builder with current values. + * + *

This method is used to create copies of the class during normal operations (e.g. when calling the + * {@link SqlColumn#as(String)} method). Any subclass of {@code SqlColumn} MUST override this method. + * + * @return a new Builder instance with all current values populated + */ + protected AbstractBuilder copyBuilder() { + return populateBaseBuilder(new Builder<>()); } - private String applyTableAlias(String tableAlias) { - return tableAlias + "." + name(); //$NON-NLS-1$ + @SuppressWarnings("unchecked") + protected > S cast(SqlColumn column) { + return (S) column; } - + + /** + * This method will add all current attributes to the specified builder. It is useful when creating + * new class instances that only change one attribute - we set all current attributes, then + * change the one attribute. This utility can be used with the with* methods and other methods that + * create new instances. + * + * @param the concrete builder type + * @return the populated builder + */ + @SuppressWarnings("unchecked") + protected > B populateBaseBuilder(B builder) { + return (B) builder + .withName(this.name) + .withTable(this.table) + .withJdbcType(this.jdbcType) + .withDescendingPhrase(this.descendingPhrase) + .withAlias(this.alias) + .withTypeHandler(this.typeHandler) + .withRenderingStrategy(this.renderingStrategy) + .withParameterTypeConverter(this.parameterTypeConverter) + .withTableQualifier(this.tableQualifier) + .withJavaType(this.javaType) + .withJavaProperty(this.javaProperty); + } + public static SqlColumn of(String name, SqlTable table) { - return SqlColumn.withName(name) + return new Builder().withName(name) .withTable(table) .build(); } - + public static SqlColumn of(String name, SqlTable table, JDBCType jdbcType) { - return SqlColumn.withName(name) + return new Builder().withName(name) .withTable(table) .withJdbcType(jdbcType) .build(); } - - public static Builder withName(String name) { - return new Builder().withName(name); - } - - public static class Builder { - private SqlTable table; - private String name; - private JDBCType jdbcType; - private String typeHandler; - - public Builder withTable(SqlTable table) { - this.table = table; - return this; - } - - public Builder withName(String name) { + + public abstract static class AbstractBuilder, B extends AbstractBuilder> { + protected @Nullable String name; + protected @Nullable SqlTable table; + protected @Nullable JDBCType jdbcType; + protected String descendingPhrase = ""; //$NON-NLS-1$ + protected @Nullable String alias; + protected @Nullable String typeHandler; + protected @Nullable RenderingStrategy renderingStrategy; + protected ParameterTypeConverter parameterTypeConverter = v -> v; + protected @Nullable String tableQualifier; + protected @Nullable Class javaType; + protected @Nullable String javaProperty; + + public B withName(String name) { this.name = name; - return this; + return getThis(); } - - public Builder withJdbcType(JDBCType jdbcType) { + + public B withTable(SqlTable table) { + this.table = table; + return getThis(); + } + + public B withJdbcType(@Nullable JDBCType jdbcType) { this.jdbcType = jdbcType; - return this; + return getThis(); + } + + public B withDescendingPhrase(String descendingPhrase) { + this.descendingPhrase = descendingPhrase; + return getThis(); + } + + public B withAlias(@Nullable String alias) { + this.alias = alias; + return getThis(); } - - public Builder withTypeHandler(String typeHandler) { + + public B withTypeHandler(@Nullable String typeHandler) { this.typeHandler = typeHandler; - return this; + return getThis(); + } + + public B withRenderingStrategy(@Nullable RenderingStrategy renderingStrategy) { + this.renderingStrategy = renderingStrategy; + return getThis(); + } + + public B withParameterTypeConverter(ParameterTypeConverter parameterTypeConverter) { + this.parameterTypeConverter = parameterTypeConverter; + return getThis(); + } + + public B withTableQualifier(@Nullable String tableQualifier) { + this.tableQualifier = tableQualifier; + return getThis(); } - - public SqlColumn build() { + + public B withJavaType(@Nullable Class javaType) { + this.javaType = javaType; + return getThis(); + } + + public B withJavaProperty(@Nullable String javaProperty) { + this.javaProperty = javaProperty; + return getThis(); + } + + protected abstract B getThis(); + + public abstract C build(); + } + + public static class Builder extends AbstractBuilder, Builder> { + @Override + public SqlColumn build() { return new SqlColumn<>(this); } + + @Override + protected Builder getThis() { + return this; + } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlCriterion.java b/src/main/java/org/mybatis/dynamic/sql/SqlCriterion.java index b06fc9f53..2989f3125 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SqlCriterion.java +++ b/src/main/java/org/mybatis/dynamic/sql/SqlCriterion.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -16,74 +16,31 @@ package org.mybatis.dynamic.sql; import java.util.ArrayList; +import java.util.Collections; import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.function.Function; -import java.util.stream.Stream; -public class SqlCriterion { - - private BindableColumn column; - private VisitableCondition condition; - private String connector; - private List> subCriteria; - - private SqlCriterion(Builder builder) { - connector = builder.connector; - column = Objects.requireNonNull(builder.column); - condition = Objects.requireNonNull(builder.condition); - subCriteria = Objects.requireNonNull(builder.subCriteria); - } - - public Optional connector() { - return Optional.ofNullable(connector); - } - - public BindableColumn column() { - return column; - } - - public VisitableCondition condition() { - return condition; - } - - public Stream mapSubCriteria(Function, R> mapper) { - return subCriteria.stream().map(mapper); +public abstract class SqlCriterion { + + private final List subCriteria = new ArrayList<>(); + + protected SqlCriterion(AbstractBuilder builder) { + subCriteria.addAll(builder.subCriteria); } - - public static Builder withColumn(BindableColumn column) { - return new Builder().withColumn(column); + + public List subCriteria() { + return Collections.unmodifiableList(subCriteria); } - public static class Builder { - private String connector; - private BindableColumn column; - private VisitableCondition condition; - private List> subCriteria = new ArrayList<>(); - - public Builder withConnector(String connector) { - this.connector = connector; - return this; - } - - public Builder withColumn(BindableColumn column) { - this.column = column; - return this; - } - - public Builder withCondition(VisitableCondition condition) { - this.condition = condition; - return this; - } - - public Builder withSubCriteria(List> subCriteria) { + public abstract R accept(SqlCriterionVisitor visitor); + + protected abstract static class AbstractBuilder> { + private final List subCriteria = new ArrayList<>(); + + public T withSubCriteria(List subCriteria) { this.subCriteria.addAll(subCriteria); - return this; - } - - public SqlCriterion build() { - return new SqlCriterion<>(this); + return getThis(); } + + protected abstract T getThis(); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlCriterionVisitor.java b/src/main/java/org/mybatis/dynamic/sql/SqlCriterionVisitor.java new file mode 100644 index 000000000..8431568b1 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/SqlCriterionVisitor.java @@ -0,0 +1,26 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql; + +public interface SqlCriterionVisitor { + R visit(ColumnAndConditionCriterion criterion); + + R visit(ExistsCriterion criterion); + + R visit(CriteriaGroup criterion); + + R visit(NotCriterion criterion); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlTable.java b/src/main/java/org/mybatis/dynamic/sql/SqlTable.java index e5d5beed8..fab3d8c2a 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SqlTable.java +++ b/src/main/java/org/mybatis/dynamic/sql/SqlTable.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -18,90 +18,45 @@ import java.sql.JDBCType; import java.util.Objects; import java.util.Optional; -import java.util.function.Supplier; -import org.jetbrains.annotations.NotNull; -public class SqlTable { - - private Supplier nameSupplier; +public class SqlTable implements TableExpression { - protected SqlTable(String tableName) { - Objects.requireNonNull(tableName); - - this.nameSupplier = () -> tableName; - } - - protected SqlTable(Supplier tableNameSupplier) { - Objects.requireNonNull(tableNameSupplier); - - this.nameSupplier = tableNameSupplier; - } - - protected SqlTable(Supplier> schemaSupplier, String tableName) { - this(Optional::empty, schemaSupplier, tableName); - } - - protected SqlTable(Supplier> catalogSupplier, Supplier> schemaSupplier, - String tableName) { - Objects.requireNonNull(catalogSupplier); - Objects.requireNonNull(schemaSupplier); - Objects.requireNonNull(tableName); - - this.nameSupplier = () -> compose(catalogSupplier, schemaSupplier, tableName); - } - - private String compose(Supplier> catalogSupplier, Supplier> schemaSupplier, - String tableName) { - return catalogSupplier.get().map(c -> compose(c, schemaSupplier, tableName)) - .orElseGet(() -> compose(schemaSupplier, tableName)); - } - - private String compose(String catalog, Supplier> schemaSupplier, String tableName) { - return schemaSupplier.get().map(s -> composeCatalogSchemaAndAndTable(catalog, s, tableName)) - .orElseGet(() -> composeCatalogAndTable(catalog, tableName)); - } + protected String tableName; - private String compose(Supplier> schemaSupplier, String tableName) { - return schemaSupplier.get().map(s -> composeSchemaAndTable(s, tableName)) - .orElse(tableName); - } - - private String composeCatalogAndTable(String catalog, String tableName) { - return catalog + ".." + tableName; //$NON-NLS-1$ + protected SqlTable(String tableName) { + this.tableName = Objects.requireNonNull(tableName); } - private String composeSchemaAndTable(String schema, String tableName) { - return schema + "." + tableName; //$NON-NLS-1$ + public String tableName() { + return tableName; } - private String composeCatalogSchemaAndAndTable(String catalog, String schema, String tableName) { - return catalog + "." + schema + "." + tableName; //$NON-NLS-1$ //$NON-NLS-2$ - } - - public String tableNameAtRuntime() { - return nameSupplier.get(); - } - - public SqlColumn allColumns() { + public BasicColumn allColumns() { return SqlColumn.of("*", this); //$NON-NLS-1$ } - @NotNull public SqlColumn column(String name) { return SqlColumn.of(name, this); } - @NotNull public SqlColumn column(String name, JDBCType jdbcType) { return SqlColumn.of(name, this, jdbcType); } - @NotNull public SqlColumn column(String name, JDBCType jdbcType, String typeHandler) { SqlColumn column = SqlColumn.of(name, this, jdbcType); return column.withTypeHandler(typeHandler); } - + + @Override + public R accept(TableExpressionVisitor visitor) { + return visitor.visit(this); + } + + public Optional tableAlias() { + return Optional.empty(); + } + public static SqlTable of(String name) { return new SqlTable(name); } diff --git a/src/main/java/org/mybatis/dynamic/sql/StringConstant.java b/src/main/java/org/mybatis/dynamic/sql/StringConstant.java index 00361e562..ae9f0a75b 100644 --- a/src/main/java/org/mybatis/dynamic/sql/StringConstant.java +++ b/src/main/java/org/mybatis/dynamic/sql/StringConstant.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -18,15 +18,23 @@ import java.util.Objects; import java.util.Optional; -import org.mybatis.dynamic.sql.render.TableAliasCalculator; +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.StringUtilities; -public class StringConstant implements BasicColumn { +public class StringConstant implements BindableColumn { + + private final @Nullable String alias; + private final String value; - private String alias; - private String value; - private StringConstant(String value) { + this(value, null); + } + + private StringConstant(String value, @Nullable String alias) { this.value = Objects.requireNonNull(value); + this.alias = alias; } @Override @@ -35,17 +43,15 @@ public Optional alias() { } @Override - public String renderWithTableAlias(TableAliasCalculator tableAliasCalculator) { - return "'" + value + "'"; //$NON-NLS-1$ //$NON-NLS-2$ + public FragmentAndParameters render(RenderingContext renderingContext) { + return FragmentAndParameters.fromFragment(StringUtilities.formatConstantForSQL(value)); } @Override public StringConstant as(String alias) { - StringConstant copy = new StringConstant(value); - copy.alias = alias; - return copy; + return new StringConstant(value, alias); } - + public static StringConstant of(String value) { return new StringConstant(value); } diff --git a/src/main/java/org/mybatis/dynamic/sql/SubQueryColumn.java b/src/main/java/org/mybatis/dynamic/sql/SubQueryColumn.java new file mode 100644 index 000000000..cc35bdcad --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/SubQueryColumn.java @@ -0,0 +1,60 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql; + +import java.util.Objects; +import java.util.Optional; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.select.SelectModel; +import org.mybatis.dynamic.sql.select.render.SubQueryRenderer; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + +public class SubQueryColumn implements BasicColumn { + private final SelectModel selectModel; + private @Nullable String alias; + + private SubQueryColumn(SelectModel selectModel) { + this.selectModel = Objects.requireNonNull(selectModel); + } + + @Override + public Optional alias() { + return Optional.ofNullable(alias); + } + + @Override + public SubQueryColumn as(String alias) { + SubQueryColumn answer = new SubQueryColumn(selectModel); + answer.alias = alias; + return answer; + } + + @Override + public FragmentAndParameters render(RenderingContext renderingContext) { + return SubQueryRenderer.withSelectModel(selectModel) + .withRenderingContext(renderingContext) + .withPrefix("(") //$NON-NLS-1$ + .withSuffix(")") //$NON-NLS-1$ + .build() + .render(); + } + + public static SubQueryColumn of(SelectModel selectModel) { + return new SubQueryColumn(selectModel); + } +} diff --git a/src/test/java/examples/schema_supplier/SchemaSupplier.java b/src/main/java/org/mybatis/dynamic/sql/TableExpression.java similarity index 57% rename from src/test/java/examples/schema_supplier/SchemaSupplier.java rename to src/main/java/org/mybatis/dynamic/sql/TableExpression.java index 6c40e5707..75ee1e8d4 100644 --- a/src/test/java/examples/schema_supplier/SchemaSupplier.java +++ b/src/main/java/org/mybatis/dynamic/sql/TableExpression.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -13,14 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package examples.schema_supplier; +package org.mybatis.dynamic.sql; -import java.util.Optional; +public interface TableExpression { -public class SchemaSupplier { - public static final String schema_property = "schemaToUse"; + R accept(TableExpressionVisitor visitor); - public static Optional schemaPropertyReader() { - return Optional.ofNullable(System.getProperty(schema_property)); + default boolean isSubQuery() { + return false; } } diff --git a/src/main/java/org/mybatis/dynamic/sql/TableExpressionVisitor.java b/src/main/java/org/mybatis/dynamic/sql/TableExpressionVisitor.java new file mode 100644 index 000000000..407d7ebbb --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/TableExpressionVisitor.java @@ -0,0 +1,24 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql; + +import org.mybatis.dynamic.sql.select.SubQuery; + +public interface TableExpressionVisitor { + R visit(SqlTable table); + + R visit(SubQuery subQuery); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/VisitableCondition.java b/src/main/java/org/mybatis/dynamic/sql/VisitableCondition.java index c3e8d165a..9969c3997 100644 --- a/src/main/java/org/mybatis/dynamic/sql/VisitableCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/VisitableCondition.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,17 +15,22 @@ */ package org.mybatis.dynamic.sql; -@FunctionalInterface -public interface VisitableCondition { - R accept(ConditionVisitor visitor); +import org.mybatis.dynamic.sql.render.RenderingContext; - /** - * Subclasses can override this to inform the renderer if the condition should not be included - * in the rendered SQL. For example, IsEqualWhenPresent will not render if the value is null. - * - * @return true if the condition should render. - */ - default boolean shouldRender() { - return true; - } -} +/** + * Deprecated interface. + * + *

Conditions are no longer rendered with a visitor, so the name is misleading. This change makes it far easier + * to implement custom conditions for functionality not supplied out of the box by the library. + * + *

If you created any direct implementations of this interface, you will need to change the rendering functions. + * The library now calls {@link RenderableCondition#renderCondition(RenderingContext, BindableColumn)} and + * {@link RenderableCondition#renderLeftColumn(RenderingContext, BindableColumn)} instead of the previous methods + * like operator, value, etc. Subclasses of the supplied abstract conditions should continue + * to function as before. + * + * @param the Java type related to the column this condition relates to. Used primarily for compiler type checking + * @deprecated since 2.0.0. Please use {@link RenderableCondition} instead. + */ +@Deprecated(since = "2.0.0", forRemoval = true) +public interface VisitableCondition extends RenderableCondition { } diff --git a/src/main/java/org/mybatis/dynamic/sql/common/AbstractBooleanExpressionDSL.java b/src/main/java/org/mybatis/dynamic/sql/common/AbstractBooleanExpressionDSL.java new file mode 100644 index 000000000..2f817fb5f --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/common/AbstractBooleanExpressionDSL.java @@ -0,0 +1,162 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.common; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.AndOrCriteriaGroup; +import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.ColumnAndConditionCriterion; +import org.mybatis.dynamic.sql.CriteriaGroup; +import org.mybatis.dynamic.sql.ExistsCriterion; +import org.mybatis.dynamic.sql.ExistsPredicate; +import org.mybatis.dynamic.sql.RenderableCondition; +import org.mybatis.dynamic.sql.SqlCriterion; +import org.mybatis.dynamic.sql.util.Validator; + +public abstract class AbstractBooleanExpressionDSL> { + private @Nullable SqlCriterion initialCriterion; + protected final List subCriteria = new ArrayList<>(); + + public T and(BindableColumn column, RenderableCondition condition, + AndOrCriteriaGroup... subCriteria) { + return and(column, condition, Arrays.asList(subCriteria)); + } + + public T and(BindableColumn column, RenderableCondition condition, + List subCriteria) { + addSubCriteria("and", buildCriterion(column, condition), subCriteria); //$NON-NLS-1$ + return getThis(); + } + + public T and(ExistsPredicate existsPredicate, AndOrCriteriaGroup... subCriteria) { + return and(existsPredicate, Arrays.asList(subCriteria)); + } + + public T and(ExistsPredicate existsPredicate, List subCriteria) { + addSubCriteria("and", buildCriterion(existsPredicate), subCriteria); //$NON-NLS-1$ + return getThis(); + } + + public T and(SqlCriterion initialCriterion, AndOrCriteriaGroup... subCriteria) { + return and(initialCriterion, Arrays.asList(subCriteria)); + } + + public T and(SqlCriterion initialCriterion, List subCriteria) { + addSubCriteria("and", buildCriterion(initialCriterion), subCriteria); //$NON-NLS-1$ + return getThis(); + } + + public T and(List criteria) { + addSubCriteria("and", criteria); //$NON-NLS-1$ + return getThis(); + } + + public T or(BindableColumn column, RenderableCondition condition, + AndOrCriteriaGroup... subCriteria) { + return or(column, condition, Arrays.asList(subCriteria)); + } + + public T or(BindableColumn column, RenderableCondition condition, + List subCriteria) { + addSubCriteria("or", buildCriterion(column, condition), subCriteria); //$NON-NLS-1$ + return getThis(); + } + + public T or(ExistsPredicate existsPredicate, AndOrCriteriaGroup... subCriteria) { + return or(existsPredicate, Arrays.asList(subCriteria)); + } + + public T or(ExistsPredicate existsPredicate, List subCriteria) { + addSubCriteria("or", buildCriterion(existsPredicate), subCriteria); //$NON-NLS-1$ + return getThis(); + } + + public T or(SqlCriterion initialCriterion, AndOrCriteriaGroup... subCriteria) { + return or(initialCriterion, Arrays.asList(subCriteria)); + } + + public T or(SqlCriterion initialCriterion, List subCriteria) { + addSubCriteria("or", buildCriterion(initialCriterion), subCriteria); //$NON-NLS-1$ + return getThis(); + } + + public T or(List criteria) { + addSubCriteria("or", criteria); //$NON-NLS-1$ + return getThis(); + } + + private SqlCriterion buildCriterion(BindableColumn column, RenderableCondition condition) { + return ColumnAndConditionCriterion.withColumn(column).withCondition(condition).build(); + } + + private SqlCriterion buildCriterion(ExistsPredicate existsPredicate) { + return new ExistsCriterion.Builder().withExistsPredicate(existsPredicate).build(); + } + + private SqlCriterion buildCriterion(SqlCriterion initialCriterion) { + return new CriteriaGroup.Builder().withInitialCriterion(initialCriterion).build(); + } + + private void addSubCriteria(String connector, SqlCriterion initialCriterion, + List subCriteria) { + this.subCriteria.add(new AndOrCriteriaGroup.Builder() + .withInitialCriterion(initialCriterion) + .withConnector(connector) + .withSubCriteria(subCriteria) + .build()); + } + + private void addSubCriteria(String connector, List criteria) { + this.subCriteria.add(new AndOrCriteriaGroup.Builder() + .withConnector(connector) + .withSubCriteria(criteria) + .build()); + } + + protected void setInitialCriterion(@Nullable SqlCriterion initialCriterion) { + this.initialCriterion = initialCriterion; + } + + protected void setInitialCriterion(@Nullable SqlCriterion initialCriterion, StatementType statementType) { + Validator.assertTrue(this.initialCriterion == null, statementType.messageNumber()); + setInitialCriterion(initialCriterion); + } + + protected @Nullable SqlCriterion getInitialCriterion() { + return initialCriterion; + } + + protected abstract T getThis(); + + public enum StatementType { + WHERE("ERROR.32"), //$NON-NLS-1$ + HAVING("ERROR.31"); //$NON-NLS-1$ + + private final String messageNumber; + + public String messageNumber() { + return messageNumber; + } + + StatementType(String messageNumber) { + this.messageNumber = messageNumber; + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/common/AbstractBooleanExpressionModel.java b/src/main/java/org/mybatis/dynamic/sql/common/AbstractBooleanExpressionModel.java new file mode 100644 index 000000000..edda8707f --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/common/AbstractBooleanExpressionModel.java @@ -0,0 +1,60 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.common; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.AndOrCriteriaGroup; +import org.mybatis.dynamic.sql.SqlCriterion; + +public abstract class AbstractBooleanExpressionModel { + private final @Nullable SqlCriterion initialCriterion; + private final List subCriteria ; + + protected AbstractBooleanExpressionModel(AbstractBuilder builder) { + initialCriterion = builder.initialCriterion; + subCriteria = builder.subCriteria; + } + + public Optional initialCriterion() { + return Optional.ofNullable(initialCriterion); + } + + public List subCriteria() { + return Collections.unmodifiableList(subCriteria); + } + + public abstract static class AbstractBuilder> { + private @Nullable SqlCriterion initialCriterion; + private final List subCriteria = new ArrayList<>(); + + public T withInitialCriterion(@Nullable SqlCriterion initialCriterion) { + this.initialCriterion = initialCriterion; + return getThis(); + } + + public T withSubCriteria(List subCriteria) { + this.subCriteria.addAll(subCriteria); + return getThis(); + } + + protected abstract T getThis(); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/common/AbstractBooleanExpressionRenderer.java b/src/main/java/org/mybatis/dynamic/sql/common/AbstractBooleanExpressionRenderer.java new file mode 100644 index 000000000..a006b1834 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/common/AbstractBooleanExpressionRenderer.java @@ -0,0 +1,101 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.common; + +import static org.mybatis.dynamic.sql.util.StringUtilities.spaceAfter; + +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.SqlCriterion; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.FragmentCollector; +import org.mybatis.dynamic.sql.where.render.CriterionRenderer; +import org.mybatis.dynamic.sql.where.render.RenderedCriterion; + +public abstract class AbstractBooleanExpressionRenderer { + protected final AbstractBooleanExpressionModel model; + private final String prefix; + private final CriterionRenderer criterionRenderer; + protected final RenderingContext renderingContext; + + protected AbstractBooleanExpressionRenderer(String prefix, AbstractBuilder builder) { + model = Objects.requireNonNull(builder.model); + this.prefix = Objects.requireNonNull(prefix); + renderingContext = Objects.requireNonNull(builder.renderingContext); + criterionRenderer = new CriterionRenderer(renderingContext); + } + + public Optional render() { + return model.initialCriterion() + .map(this::renderWithInitialCriterion) + .orElseGet(this::renderWithoutInitialCriterion) + .map(RenderedCriterion::fragmentAndParameters); + } + + private Optional renderWithInitialCriterion(SqlCriterion initialCriterion) { + return criterionRenderer.render(initialCriterion, model.subCriteria(), this::calculateClause); + } + + private Optional renderWithoutInitialCriterion() { + return criterionRenderer.render(model.subCriteria(), this::calculateClause); + } + + private String calculateClause(FragmentCollector collector) { + if (collector.hasMultipleFragments()) { + return collector.collectFragments( + Collectors.joining(" ", spaceAfter(prefix), "")); //$NON-NLS-1$ //$NON-NLS-2$ + } else { + return collector.firstFragment() + .map(this::stripEnclosingParenthesesIfPresent) + .map(this::addPrefix) + .orElse(""); //$NON-NLS-1$ + } + } + + private String stripEnclosingParenthesesIfPresent(String fragment) { + // The fragment will have surrounding open/close parentheses if there is more than one rendered condition. + // Since there is only a single fragment, we don't need these in the final rendered clause + if (fragment.startsWith("(") && fragment.endsWith(")")) { //$NON-NLS-1$ //$NON-NLS-2$ + return fragment.substring(1, fragment.length() - 1); + } else { + return fragment; + } + } + + private String addPrefix(String fragment) { + return spaceAfter(prefix) + fragment; + } + + public abstract static class AbstractBuilder> { + private final AbstractBooleanExpressionModel model; + private @Nullable RenderingContext renderingContext; + + protected AbstractBuilder(AbstractBooleanExpressionModel model) { + this.model = model; + } + + public B withRenderingContext(RenderingContext renderingContext) { + this.renderingContext = renderingContext; + return getThis(); + } + + protected abstract B getThis(); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/common/CommonBuilder.java b/src/main/java/org/mybatis/dynamic/sql/common/CommonBuilder.java new file mode 100644 index 000000000..269b857af --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/common/CommonBuilder.java @@ -0,0 +1,91 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.common; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.SqlTable; +import org.mybatis.dynamic.sql.configuration.StatementConfiguration; +import org.mybatis.dynamic.sql.where.EmbeddedWhereModel; + +/** + * Builder class shared between the delete and update model builders. + * + * @param type of the implementing builder + */ +public abstract class CommonBuilder> { + private @Nullable SqlTable table; + private @Nullable String tableAlias; + private @Nullable EmbeddedWhereModel whereModel; + private @Nullable Long limit; + private @Nullable OrderByModel orderByModel; + private @Nullable StatementConfiguration statementConfiguration; + + public @Nullable SqlTable table() { + return table; + } + + public @Nullable String tableAlias() { + return tableAlias; + } + + public @Nullable EmbeddedWhereModel whereModel() { + return whereModel; + } + + public @Nullable Long limit() { + return limit; + } + + public @Nullable OrderByModel orderByModel() { + return orderByModel; + } + + public @Nullable StatementConfiguration statementConfiguration() { + return statementConfiguration; + } + + public T withTable(SqlTable table) { + this.table = table; + return getThis(); + } + + public T withTableAlias(@Nullable String tableAlias) { + this.tableAlias = tableAlias; + return getThis(); + } + + public T withWhereModel(@Nullable EmbeddedWhereModel whereModel) { + this.whereModel = whereModel; + return getThis(); + } + + public T withLimit(@Nullable Long limit) { + this.limit = limit; + return getThis(); + } + + public T withOrderByModel(@Nullable OrderByModel orderByModel) { + this.orderByModel = orderByModel; + return getThis(); + } + + public T withStatementConfiguration(StatementConfiguration statementConfiguration) { + this.statementConfiguration = statementConfiguration; + return getThis(); + } + + protected abstract T getThis(); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/OrderByModel.java b/src/main/java/org/mybatis/dynamic/sql/common/OrderByModel.java similarity index 51% rename from src/main/java/org/mybatis/dynamic/sql/select/OrderByModel.java rename to src/main/java/org/mybatis/dynamic/sql/common/OrderByModel.java index 5cecd2f28..42d2de4fb 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/OrderByModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/common/OrderByModel.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -13,28 +13,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.mybatis.dynamic.sql.select; +package org.mybatis.dynamic.sql.common; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collection; import java.util.List; -import java.util.function.Function; +import java.util.Objects; import java.util.stream.Stream; import org.mybatis.dynamic.sql.SortSpecification; +import org.mybatis.dynamic.sql.util.Validator; public class OrderByModel { - private List columns = new ArrayList<>(); - - private OrderByModel(List columns) { + private final List columns = new ArrayList<>(); + + private OrderByModel(Collection columns) { + Objects.requireNonNull(columns); + Validator.assertNotEmpty(columns, "ERROR.12"); //$NON-NLS-1$ this.columns.addAll(columns); } - - public Stream mapColumns(Function mapper) { - return columns.stream().map(mapper); + + public Stream columns() { + return columns.stream(); } - - public static OrderByModel of(SortSpecification...columns) { - return new OrderByModel(Arrays.asList(columns)); + + public static OrderByModel of(Collection columns) { + return new OrderByModel(columns); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/common/OrderByRenderer.java b/src/main/java/org/mybatis/dynamic/sql/common/OrderByRenderer.java new file mode 100644 index 000000000..851bf73ea --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/common/OrderByRenderer.java @@ -0,0 +1,38 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.common; + +import java.util.Objects; +import java.util.stream.Collectors; + +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.FragmentCollector; + +public class OrderByRenderer { + private final RenderingContext renderingContext; + + public OrderByRenderer(RenderingContext renderingContext) { + this.renderingContext = Objects.requireNonNull(renderingContext); + } + + public FragmentAndParameters render(OrderByModel orderByModel) { + return orderByModel.columns().map(c -> c.renderForOrderBy(renderingContext)) + .collect(FragmentCollector.collect()) + .toFragmentAndParameters( + Collectors.joining(", ", "order by ", "")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } +} diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/AddressRecord.kt b/src/main/java/org/mybatis/dynamic/sql/common/package-info.java similarity index 65% rename from src/test/kotlin/examples/kotlin/spring/canonical/AddressRecord.kt rename to src/main/java/org/mybatis/dynamic/sql/common/package-info.java index 4c81267ab..f581c8034 100644 --- a/src/test/kotlin/examples/kotlin/spring/canonical/AddressRecord.kt +++ b/src/main/java/org/mybatis/dynamic/sql/common/package-info.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package examples.kotlin.spring.canonical +@NullMarked +package org.mybatis.dynamic.sql.common; -data class AddressRecord(var id: Int? = null, var streetAddress: String? = null, var city: String? = null, var state: String? = null) +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/mybatis/dynamic/sql/configuration/GlobalConfiguration.java b/src/main/java/org/mybatis/dynamic/sql/configuration/GlobalConfiguration.java new file mode 100644 index 000000000..670ddd935 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/configuration/GlobalConfiguration.java @@ -0,0 +1,70 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.configuration; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; +import java.util.Properties; + +import org.mybatis.dynamic.sql.exception.DynamicSqlException; +import org.mybatis.dynamic.sql.util.Messages; + +public class GlobalConfiguration { + public static final String CONFIGURATION_FILE_PROPERTY = "mybatis-dynamic-sql.configurationFile"; //$NON-NLS-1$ + private static final String DEFAULT_PROPERTY_FILE = "mybatis-dynamic-sql.properties"; //$NON-NLS-1$ + private boolean isNonRenderingWhereClauseAllowed = false; + private final Properties properties = new Properties(); + + public GlobalConfiguration() { + initialize(); + } + + private void initialize() { + initializeProperties(); + initializeKnownProperties(); + } + + private void initializeProperties() { + String configFileName = getConfigurationFileName(); + InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(configFileName); + if (inputStream != null) { + loadProperties(inputStream, configFileName); + } + } + + private String getConfigurationFileName() { + String property = System.getProperty(CONFIGURATION_FILE_PROPERTY); + return Objects.requireNonNullElse(property, DEFAULT_PROPERTY_FILE); + } + + void loadProperties(InputStream inputStream, String propertyFile) { + try { + properties.load(inputStream); + } catch (IOException e) { + throw new DynamicSqlException(Messages.getString("ERROR.3", propertyFile), e); //$NON-NLS-1$ + } + } + + private void initializeKnownProperties() { + String value = properties.getProperty("nonRenderingWhereClauseAllowed", "false"); //$NON-NLS-1$ //$NON-NLS-2$ + isNonRenderingWhereClauseAllowed = Boolean.parseBoolean(value); + } + + public boolean isIsNonRenderingWhereClauseAllowed() { + return isNonRenderingWhereClauseAllowed; + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/configuration/GlobalContext.java b/src/main/java/org/mybatis/dynamic/sql/configuration/GlobalContext.java new file mode 100644 index 000000000..6963374b6 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/configuration/GlobalContext.java @@ -0,0 +1,29 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.configuration; + +public class GlobalContext { + + private static final GlobalContext instance = new GlobalContext(); + + private final GlobalConfiguration globalConfiguration = new GlobalConfiguration(); + + private GlobalContext() {} + + public static GlobalConfiguration getConfiguration() { + return instance.globalConfiguration; + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/configuration/StatementConfiguration.java b/src/main/java/org/mybatis/dynamic/sql/configuration/StatementConfiguration.java new file mode 100644 index 000000000..ed187fe92 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/configuration/StatementConfiguration.java @@ -0,0 +1,55 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.configuration; + +import org.mybatis.dynamic.sql.exception.NonRenderingWhereClauseException; + +/** + * This class can be used to change some behaviors of the framework. Every configurable statement + * contains a unique instance of this class, so changes here will only impact a single statement. + * If you intend to change the behavior for all statements, use the {@link GlobalConfiguration}. + * Initial values for this class in each statement are set from the {@link GlobalConfiguration}. + * Configurable behaviors are detailed below: + * + *

+ *
nonRenderingWhereClauseAllowed
+ *
If false (default), the framework will throw a {@link NonRenderingWhereClauseException} + * if a where clause is specified in the statement, but it fails to render because all + * optional conditions do not render. For example, if an "in" condition specifies an + * empty list of values. If no criteria are specified in a where clause, the framework + * assumes that no where clause was intended and will not throw an exception. + *
+ *
+ * + * @see GlobalConfiguration + * + * @since 1.4.1 + * + * @author Jeff Butler + */ +public class StatementConfiguration { + private boolean isNonRenderingWhereClauseAllowed = + GlobalContext.getConfiguration().isIsNonRenderingWhereClauseAllowed(); + + public boolean isNonRenderingWhereClauseAllowed() { + return isNonRenderingWhereClauseAllowed; + } + + public StatementConfiguration setNonRenderingWhereClauseAllowed(boolean nonRenderingWhereClauseAllowed) { + isNonRenderingWhereClauseAllowed = nonRenderingWhereClauseAllowed; + return this; + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/configuration/package-info.java b/src/main/java/org/mybatis/dynamic/sql/configuration/package-info.java new file mode 100644 index 000000000..41fcac6c7 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/configuration/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +@NullMarked +package org.mybatis.dynamic.sql.configuration; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/mybatis/dynamic/sql/delete/DeleteDSL.java b/src/main/java/org/mybatis/dynamic/sql/delete/DeleteDSL.java index 231e21398..aeebc5498 100644 --- a/src/main/java/org/mybatis/dynamic/sql/delete/DeleteDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/delete/DeleteDSL.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,103 +15,136 @@ */ package org.mybatis.dynamic.sql.delete; +import java.util.Arrays; +import java.util.Collection; import java.util.Objects; +import java.util.function.Consumer; import java.util.function.Function; -import java.util.function.ToIntFunction; -import org.mybatis.dynamic.sql.BindableColumn; -import org.mybatis.dynamic.sql.SqlCriterion; +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.SortSpecification; import org.mybatis.dynamic.sql.SqlTable; -import org.mybatis.dynamic.sql.VisitableCondition; -import org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider; +import org.mybatis.dynamic.sql.common.OrderByModel; +import org.mybatis.dynamic.sql.configuration.StatementConfiguration; import org.mybatis.dynamic.sql.util.Buildable; -import org.mybatis.dynamic.sql.util.mybatis3.MyBatis3Utils; -import org.mybatis.dynamic.sql.where.AbstractWhereDSL; -import org.mybatis.dynamic.sql.where.WhereApplier; -import org.mybatis.dynamic.sql.where.WhereModel; - -public class DeleteDSL implements Buildable { - - private Function adapterFunction; - private SqlTable table; - private DeleteWhereBuilder whereBuilder = new DeleteWhereBuilder(); - - private DeleteDSL(SqlTable table, Function adapterFunction) { +import org.mybatis.dynamic.sql.where.AbstractWhereFinisher; +import org.mybatis.dynamic.sql.where.AbstractWhereStarter; +import org.mybatis.dynamic.sql.where.EmbeddedWhereModel; + +public class DeleteDSL implements AbstractWhereStarter.DeleteWhereBuilder, DeleteDSL>, + Buildable { + + private final Function adapterFunction; + private final SqlTable table; + private final @Nullable String tableAlias; + private @Nullable DeleteWhereBuilder whereBuilder; + private final StatementConfiguration statementConfiguration = new StatementConfiguration(); + private @Nullable Long limit; + private @Nullable OrderByModel orderByModel; + + private DeleteDSL(SqlTable table, @Nullable String tableAlias, Function adapterFunction) { this.table = Objects.requireNonNull(table); + this.tableAlias = tableAlias; this.adapterFunction = Objects.requireNonNull(adapterFunction); } - + + @Override public DeleteWhereBuilder where() { + whereBuilder = Objects.requireNonNullElseGet(whereBuilder, DeleteWhereBuilder::new); return whereBuilder; } - - public DeleteWhereBuilder where(BindableColumn column, VisitableCondition condition, - SqlCriterion...subCriteria) { - whereBuilder.where(column, condition, subCriteria); - return whereBuilder; + + public DeleteDSL limit(long limit) { + return limitWhenPresent(limit); + } + + public DeleteDSL limitWhenPresent(@Nullable Long limit) { + this.limit = limit; + return this; + } + + public DeleteDSL orderBy(SortSpecification... columns) { + return orderBy(Arrays.asList(columns)); } - public DeleteWhereBuilder applyWhere(WhereApplier whereApplier) { - return whereBuilder.applyWhere(whereApplier); + public DeleteDSL orderBy(Collection columns) { + orderByModel = OrderByModel.of(columns); + return this; } /** - * WARNING! Calling this method could result in an delete statement that deletes + * WARNING! Calling this method could result in a delete statement that deletes * all rows in a table. - * + * * @return the model class */ @Override public R build() { DeleteModel deleteModel = DeleteModel.withTable(table) - .withWhereModel(whereBuilder.buildWhereModel()) + .withTableAlias(tableAlias) + .withLimit(limit) + .withOrderByModel(orderByModel) + .withWhereModel(whereBuilder == null ? null : whereBuilder.buildWhereModel()) + .withStatementConfiguration(statementConfiguration) .build(); + return adapterFunction.apply(deleteModel); } - - public static DeleteDSL deleteFrom(Function adapterFunction, SqlTable table) { - return new DeleteDSL<>(table, adapterFunction); + + @Override + public DeleteDSL configureStatement(Consumer consumer) { + consumer.accept(statementConfiguration); + return this; + } + + public static DeleteDSL deleteFrom(Function adapterFunction, SqlTable table, + @Nullable String tableAlias) { + return new DeleteDSL<>(table, tableAlias, adapterFunction); } - + public static DeleteDSL deleteFrom(SqlTable table) { - return deleteFrom(Function.identity(), table); + return deleteFrom(Function.identity(), table, null); } - - /** - * Delete record(s) by executing a MyBatis3 mapper method. - * - * @deprecated in favor of {@link MyBatis3Utils#deleteFrom(ToIntFunction, SqlTable, DeleteDSLCompleter)}. - * This method will be removed without direct replacement in a future version - * @param return value from a delete method - typically Integer - * @param mapperMethod MyBatis3 mapper method that performs the delete - * @param table table to delete from - * @return number of records deleted - typically as Integer - */ - @Deprecated - public static DeleteDSL> deleteFromWithMapper( - Function mapperMethod, SqlTable table) { - return deleteFrom(deleteModel -> MyBatis3DeleteModelAdapter.of(deleteModel, mapperMethod), table); + + public static DeleteDSL deleteFrom(SqlTable table, String tableAlias) { + return deleteFrom(Function.identity(), table, tableAlias); } - - public class DeleteWhereBuilder extends AbstractWhereDSL implements Buildable { - + + public class DeleteWhereBuilder extends AbstractWhereFinisher implements Buildable { + private DeleteWhereBuilder() { - super(); + super(DeleteDSL.this); + } + + public DeleteDSL limit(long limit) { + return limitWhenPresent(limit); + } + + public DeleteDSL limitWhenPresent(@Nullable Long limit) { + return DeleteDSL.this.limitWhenPresent(limit); + } + + public DeleteDSL orderBy(SortSpecification... columns) { + return orderBy(Arrays.asList(columns)); + } + + public DeleteDSL orderBy(Collection columns) { + orderByModel = OrderByModel.of(columns); + return DeleteDSL.this; } @Override public R build() { return DeleteDSL.this.build(); } - + @Override protected DeleteWhereBuilder getThis() { return this; } - @Override - protected WhereModel buildWhereModel() { - return super.internalBuild(); + protected EmbeddedWhereModel buildWhereModel() { + return buildModel(); } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/delete/DeleteDSLCompleter.java b/src/main/java/org/mybatis/dynamic/sql/delete/DeleteDSLCompleter.java index 90e270c5d..b1aeee392 100644 --- a/src/main/java/org/mybatis/dynamic/sql/delete/DeleteDSLCompleter.java +++ b/src/main/java/org/mybatis/dynamic/sql/delete/DeleteDSLCompleter.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -26,36 +26,36 @@ * Represents a function that can be used to create a simplified delete method. When using this function * you can create a method that does not require a user to call the build() and render() methods - making * client code look a bit cleaner. - * + * *

This function is intended to be used in conjunction with a utility method like * {@link MyBatis3Utils#deleteFrom(ToIntFunction, SqlTable, DeleteDSLCompleter)} - * + * *

For example, you can create mapper interface methods like this: - * + * *

  * @DeleteProvider(type=SqlProviderAdapter.class, method="delete")
  * int delete(DeleteStatementProvider deleteStatement);
- *   
+ *
  * default int delete(DeleteDSLCompleter completer) {
  *     return MyBatis3Utils.deleteFrom(this::delete, person, completer);
  * }
  * 
- * + * *

And then call the simplified default method like this: - * + * *

  * int rows = mapper.delete(c ->
  *           c.where(occupation, isNull()));
  * 
- * + * *

You can implement a "delete all" with the following code: - * + * *

  * int rows = mapper.delete(c -> c);
  * 
- * + * *

Or - * + * *

  * long rows = mapper.delete(DeleteDSLCompleter.allRows());
  * 
@@ -68,7 +68,7 @@ public interface DeleteDSLCompleter extends /** * Returns a completer that can be used to delete every row in a table. - * + * * @return the completer that will delete every row in a table */ static DeleteDSLCompleter allRows() { diff --git a/src/main/java/org/mybatis/dynamic/sql/delete/DeleteModel.java b/src/main/java/org/mybatis/dynamic/sql/delete/DeleteModel.java index 67a56c1d6..1f9dc606f 100644 --- a/src/main/java/org/mybatis/dynamic/sql/delete/DeleteModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/delete/DeleteModel.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -18,56 +18,74 @@ import java.util.Objects; import java.util.Optional; -import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.Nullable; import org.mybatis.dynamic.sql.SqlTable; +import org.mybatis.dynamic.sql.common.CommonBuilder; +import org.mybatis.dynamic.sql.common.OrderByModel; +import org.mybatis.dynamic.sql.configuration.StatementConfiguration; import org.mybatis.dynamic.sql.delete.render.DeleteRenderer; import org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider; import org.mybatis.dynamic.sql.render.RenderingStrategy; -import org.mybatis.dynamic.sql.where.WhereModel; +import org.mybatis.dynamic.sql.where.EmbeddedWhereModel; public class DeleteModel { - private SqlTable table; - private WhereModel whereModel; - + private final SqlTable table; + private final @Nullable String tableAlias; + private final @Nullable EmbeddedWhereModel whereModel; + private final @Nullable Long limit; + private final @Nullable OrderByModel orderByModel; + private final StatementConfiguration statementConfiguration; + private DeleteModel(Builder builder) { - table = Objects.requireNonNull(builder.table); - whereModel = builder.whereModel; + table = Objects.requireNonNull(builder.table()); + whereModel = builder.whereModel(); + tableAlias = builder.tableAlias(); + limit = builder.limit(); + orderByModel = builder.orderByModel(); + statementConfiguration = Objects.requireNonNull(builder.statementConfiguration()); } - + public SqlTable table() { return table; } - - public Optional whereModel() { + + public Optional tableAlias() { + return Optional.ofNullable(tableAlias); + } + + public Optional whereModel() { return Optional.ofNullable(whereModel); } - @NotNull + public Optional limit() { + return Optional.ofNullable(limit); + } + + public Optional orderByModel() { + return Optional.ofNullable(orderByModel); + } + + public StatementConfiguration statementConfiguration() { + return statementConfiguration; + } + public DeleteStatementProvider render(RenderingStrategy renderingStrategy) { return DeleteRenderer.withDeleteModel(this) .withRenderingStrategy(renderingStrategy) .build() .render(); } - + public static Builder withTable(SqlTable table) { return new Builder().withTable(table); } - - public static class Builder { - private SqlTable table; - private WhereModel whereModel; - - public Builder withTable(SqlTable table) { - this.table = table; - return this; - } - - public Builder withWhereModel(WhereModel whereModel) { - this.whereModel = whereModel; + + public static class Builder extends CommonBuilder { + @Override + protected Builder getThis() { return this; } - + public DeleteModel build() { return new DeleteModel(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/delete/MyBatis3DeleteModelAdapter.java b/src/main/java/org/mybatis/dynamic/sql/delete/MyBatis3DeleteModelAdapter.java deleted file mode 100644 index 86fc913b6..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/delete/MyBatis3DeleteModelAdapter.java +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright 2016-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.delete; - -import java.util.Objects; -import java.util.function.Function; - -import org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider; -import org.mybatis.dynamic.sql.render.RenderingStrategy; - -/** - * This adapter will render the underlying delete model for MyBatis3, and then call a MyBatis mapper method. - * - * @deprecated in favor of {@link DeleteDSLCompleter}. This class will be removed without replacement in a - * future version - * - * @author Jeff Butler - * - */ -@Deprecated -public class MyBatis3DeleteModelAdapter { - - private DeleteModel deleteModel; - private Function mapperMethod; - - private MyBatis3DeleteModelAdapter(DeleteModel deleteModel, Function mapperMethod) { - this.deleteModel = Objects.requireNonNull(deleteModel); - this.mapperMethod = Objects.requireNonNull(mapperMethod); - } - - public R execute() { - return mapperMethod.apply(deleteStatement()); - } - - private DeleteStatementProvider deleteStatement() { - return deleteModel.render(RenderingStrategy.MYBATIS3); - } - - public static MyBatis3DeleteModelAdapter of(DeleteModel deleteModel, - Function mapperMethod) { - return new MyBatis3DeleteModelAdapter<>(deleteModel, mapperMethod); - } -} diff --git a/src/main/java/org/mybatis/dynamic/sql/delete/package-info.java b/src/main/java/org/mybatis/dynamic/sql/delete/package-info.java new file mode 100644 index 000000000..a7bfc9d26 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/delete/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +@NullMarked +package org.mybatis.dynamic.sql.delete; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/mybatis/dynamic/sql/delete/render/DefaultDeleteStatementProvider.java b/src/main/java/org/mybatis/dynamic/sql/delete/render/DefaultDeleteStatementProvider.java index 0be446fe8..db5feef01 100644 --- a/src/main/java/org/mybatis/dynamic/sql/delete/render/DefaultDeleteStatementProvider.java +++ b/src/main/java/org/mybatis/dynamic/sql/delete/render/DefaultDeleteStatementProvider.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -19,20 +19,22 @@ import java.util.Map; import java.util.Objects; +import org.jspecify.annotations.Nullable; + public class DefaultDeleteStatementProvider implements DeleteStatementProvider { - private String deleteStatement; - private Map parameters; - + private final String deleteStatement; + private final Map parameters; + private DefaultDeleteStatementProvider(Builder builder) { deleteStatement = Objects.requireNonNull(builder.deleteStatement); - parameters = Objects.requireNonNull(builder.parameters); + parameters = builder.parameters; } - + @Override public Map getParameters() { return parameters; } - + @Override public String getDeleteStatement() { return deleteStatement; @@ -41,23 +43,23 @@ public String getDeleteStatement() { public static Builder withDeleteStatement(String deleteStatement) { return new Builder().withDeleteStatement(deleteStatement); } - + public static class Builder { - private String deleteStatement; - private Map parameters = new HashMap<>(); - + private @Nullable String deleteStatement; + private final Map parameters = new HashMap<>(); + public Builder withDeleteStatement(String deleteStatement) { this.deleteStatement = deleteStatement; return this; } - + public Builder withParameters(Map parameters) { this.parameters.putAll(parameters); return this; } - + public DefaultDeleteStatementProvider build() { return new DefaultDeleteStatementProvider(this); } } -} \ No newline at end of file +} diff --git a/src/main/java/org/mybatis/dynamic/sql/delete/render/DeleteRenderer.java b/src/main/java/org/mybatis/dynamic/sql/delete/render/DeleteRenderer.java index 4688d29f3..d65fcdd58 100644 --- a/src/main/java/org/mybatis/dynamic/sql/delete/render/DeleteRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/delete/render/DeleteRenderer.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,83 +15,108 @@ */ package org.mybatis.dynamic.sql.delete.render; -import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore; - import java.util.Objects; import java.util.Optional; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.common.OrderByModel; +import org.mybatis.dynamic.sql.common.OrderByRenderer; import org.mybatis.dynamic.sql.delete.DeleteModel; +import org.mybatis.dynamic.sql.render.ExplicitTableAliasCalculator; +import org.mybatis.dynamic.sql.render.RenderedParameterInfo; +import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.render.RenderingStrategy; import org.mybatis.dynamic.sql.render.TableAliasCalculator; -import org.mybatis.dynamic.sql.where.WhereModel; -import org.mybatis.dynamic.sql.where.render.WhereClauseProvider; -import org.mybatis.dynamic.sql.where.render.WhereRenderer; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.FragmentCollector; +import org.mybatis.dynamic.sql.where.EmbeddedWhereModel; public class DeleteRenderer { - private DeleteModel deleteModel; - private RenderingStrategy renderingStrategy; - + private final DeleteModel deleteModel; + private final RenderingContext renderingContext; + private DeleteRenderer(Builder builder) { deleteModel = Objects.requireNonNull(builder.deleteModel); - renderingStrategy = Objects.requireNonNull(builder.renderingStrategy); + TableAliasCalculator tableAliasCalculator = builder.deleteModel.tableAlias() + .map(a -> ExplicitTableAliasCalculator.of(deleteModel.table(), a)) + .orElseGet(TableAliasCalculator::empty); + renderingContext = RenderingContext + .withRenderingStrategy(Objects.requireNonNull(builder.renderingStrategy)) + .withTableAliasCalculator(tableAliasCalculator) + .withStatementConfiguration(deleteModel.statementConfiguration()) + .build(); } - + public DeleteStatementProvider render() { - return deleteModel.whereModel() - .flatMap(this::renderWhereClause) - .map(this::renderWithWhereClause) - .orElseGet(this::renderWithoutWhereClause); + FragmentCollector fragmentCollector = new FragmentCollector(); + + fragmentCollector.add(calculateDeleteStatementStart()); + calculateWhereClause().ifPresent(fragmentCollector::add); + calculateOrderByClause().ifPresent(fragmentCollector::add); + calculateLimitClause().ifPresent(fragmentCollector::add); + + return toDeleteStatementProvider(fragmentCollector); } - - private DeleteStatementProvider renderWithWhereClause(WhereClauseProvider whereClauseProvider) { - return DefaultDeleteStatementProvider.withDeleteStatement(calculateDeleteStatement(whereClauseProvider)) - .withParameters(whereClauseProvider.getParameters()) + + private DeleteStatementProvider toDeleteStatementProvider(FragmentCollector fragmentCollector) { + return DefaultDeleteStatementProvider + .withDeleteStatement(fragmentCollector.collectFragments(Collectors.joining(" "))) //$NON-NLS-1$ + .withParameters(fragmentCollector.parameters()) .build(); } - - private String calculateDeleteStatement(WhereClauseProvider whereClause) { - return calculateDeleteStatement() - + spaceBefore(whereClause.getWhereClause()); + + private FragmentAndParameters calculateDeleteStatementStart() { + String aliasedTableName = renderingContext.aliasedTableName(deleteModel.table()); + return FragmentAndParameters.fromFragment("delete from " + aliasedTableName); //$NON-NLS-1$ } - private String calculateDeleteStatement() { - return "delete from" //$NON-NLS-1$ - + spaceBefore(deleteModel.table().tableNameAtRuntime()); + private Optional calculateWhereClause() { + return deleteModel.whereModel().flatMap(this::renderWhereClause); } - - private DeleteStatementProvider renderWithoutWhereClause() { - return DefaultDeleteStatementProvider.withDeleteStatement(calculateDeleteStatement()) + + private Optional renderWhereClause(EmbeddedWhereModel whereModel) { + return whereModel.render(renderingContext); + } + + private Optional calculateLimitClause() { + return deleteModel.limit().map(this::renderLimitClause); + } + + private FragmentAndParameters renderLimitClause(Long limit) { + RenderedParameterInfo parameterInfo = renderingContext.calculateLimitParameterInfo(); + + return FragmentAndParameters.withFragment("limit " + parameterInfo.renderedPlaceHolder()) //$NON-NLS-1$ + .withParameter(parameterInfo.parameterMapKey(), limit) .build(); } - - private Optional renderWhereClause(WhereModel whereModel) { - return WhereRenderer.withWhereModel(whereModel) - .withRenderingStrategy(renderingStrategy) - .withSequence(new AtomicInteger(1)) - .withTableAliasCalculator(TableAliasCalculator.empty()) - .build() - .render(); + + private Optional calculateOrderByClause() { + return deleteModel.orderByModel().map(this::renderOrderByClause); + } + + private FragmentAndParameters renderOrderByClause(OrderByModel orderByModel) { + return new OrderByRenderer(renderingContext).render(orderByModel); } - + public static Builder withDeleteModel(DeleteModel deleteModel) { return new Builder().withDeleteModel(deleteModel); } - + public static class Builder { - private DeleteModel deleteModel; - private RenderingStrategy renderingStrategy; + private @Nullable DeleteModel deleteModel; + private @Nullable RenderingStrategy renderingStrategy; public Builder withDeleteModel(DeleteModel deleteModel) { this.deleteModel = deleteModel; return this; } - + public Builder withRenderingStrategy(RenderingStrategy renderingStrategy) { this.renderingStrategy = renderingStrategy; return this; } - + public DeleteRenderer build() { return new DeleteRenderer(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/delete/render/DeleteStatementProvider.java b/src/main/java/org/mybatis/dynamic/sql/delete/render/DeleteStatementProvider.java index 7b0875a46..743c84e2d 100644 --- a/src/main/java/org/mybatis/dynamic/sql/delete/render/DeleteStatementProvider.java +++ b/src/main/java/org/mybatis/dynamic/sql/delete/render/DeleteStatementProvider.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -19,6 +19,6 @@ public interface DeleteStatementProvider { Map getParameters(); - + String getDeleteStatement(); -} \ No newline at end of file +} diff --git a/src/main/java/org/mybatis/dynamic/sql/delete/render/package-info.java b/src/main/java/org/mybatis/dynamic/sql/delete/render/package-info.java new file mode 100644 index 000000000..8b25d555d --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/delete/render/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +@NullMarked +package org.mybatis.dynamic.sql.delete.render; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/mybatis/dynamic/sql/exception/DuplicateTableAliasException.java b/src/main/java/org/mybatis/dynamic/sql/exception/DuplicateTableAliasException.java new file mode 100644 index 000000000..21d43a927 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/exception/DuplicateTableAliasException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.exception; + +import java.io.Serial; +import java.util.Objects; + +import org.mybatis.dynamic.sql.SqlTable; +import org.mybatis.dynamic.sql.util.Messages; + +/** + * This exception is thrown when a query is built that attempts to specify more than one + * alias for the same instance of an SqlTable object. That error that would produce a select + * statement that doesn't work. + * + *

This error usually occurs when building a self-join query. The workaround is to create + * a second instance of the SqlTable object to use in the self-join. + * + * @since 1.3.1 + * + * @author Jeff Butler + */ +public class DuplicateTableAliasException extends DynamicSqlException { + + @Serial + private static final long serialVersionUID = -2631664872557787391L; + + public DuplicateTableAliasException(SqlTable table, String newAlias, String existingAlias) { + super(generateMessage(Objects.requireNonNull(table), + Objects.requireNonNull(newAlias), + Objects.requireNonNull(existingAlias))); + } + + private static String generateMessage(SqlTable table, String newAlias, String existingAlias) { + return Messages.getString("ERROR.1", table.tableName(), newAlias, existingAlias); //$NON-NLS-1$ + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinCondition.java b/src/main/java/org/mybatis/dynamic/sql/exception/DynamicSqlException.java similarity index 52% rename from src/main/java/org/mybatis/dynamic/sql/select/join/JoinCondition.java rename to src/main/java/org/mybatis/dynamic/sql/exception/DynamicSqlException.java index 320a276e5..f1836b990 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/exception/DynamicSqlException.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -13,20 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.mybatis.dynamic.sql.select.join; +package org.mybatis.dynamic.sql.exception; -import org.mybatis.dynamic.sql.BasicColumn; +import java.io.Serial; -public abstract class JoinCondition { - private BasicColumn rightColumn; - - public JoinCondition(BasicColumn rightColumn) { - this.rightColumn = rightColumn; +public class DynamicSqlException extends RuntimeException { + @Serial + private static final long serialVersionUID = 349021672061361244L; + + public DynamicSqlException(String message) { + super(message); } - - public BasicColumn rightColumn() { - return rightColumn; + + public DynamicSqlException(String message, Throwable cause) { + super(message, cause); } - - public abstract String operator(); } diff --git a/src/main/java/org/mybatis/dynamic/sql/exception/InvalidSqlException.java b/src/main/java/org/mybatis/dynamic/sql/exception/InvalidSqlException.java new file mode 100644 index 000000000..44d407ea1 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/exception/InvalidSqlException.java @@ -0,0 +1,27 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.exception; + +import java.io.Serial; + +public class InvalidSqlException extends DynamicSqlException { + @Serial + private static final long serialVersionUID = 1666851020951347843L; + + public InvalidSqlException(String message) { + super(message); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/exception/NonRenderingWhereClauseException.java b/src/main/java/org/mybatis/dynamic/sql/exception/NonRenderingWhereClauseException.java new file mode 100644 index 000000000..0561f2d42 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/exception/NonRenderingWhereClauseException.java @@ -0,0 +1,51 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.exception; + +import java.io.Serial; + +import org.mybatis.dynamic.sql.configuration.GlobalConfiguration; +import org.mybatis.dynamic.sql.configuration.StatementConfiguration; +import org.mybatis.dynamic.sql.util.Messages; + +/** + * This exception is thrown when the where clause in a statement will not render. + * This can happen if all the optional conditions in a where clause fail to + * render - for example, if an "in" condition specifies an empty list. + * + *

By default, the framework will throw this exception if a where clause + * fails to render. A where clause that fails to render can be very dangerous in that + * it could cause all rows in a table to be affected by a statement - for example, + * all rows could be deleted. + * + *

If you intend to allow a where clause to not render, then configure the + * statement to allow it, or change the global configuration. + * + * @see GlobalConfiguration + * @see StatementConfiguration + * + * @since 1.4.1 + * + * @author Jeff Butler + */ +public class NonRenderingWhereClauseException extends DynamicSqlException { + @Serial + private static final long serialVersionUID = 6619119078542625135L; + + public NonRenderingWhereClauseException() { + super(Messages.getString("ERROR.2")); //$NON-NLS-1$ + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/exception/package-info.java b/src/main/java/org/mybatis/dynamic/sql/exception/package-info.java new file mode 100644 index 000000000..4e802e370 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/exception/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +@NullMarked +package org.mybatis.dynamic.sql.exception; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/AbstractMultiRowInsertModel.java b/src/main/java/org/mybatis/dynamic/sql/insert/AbstractMultiRowInsertModel.java index 270b92609..4fc8a3b4d 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/AbstractMultiRowInsertModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/AbstractMultiRowInsertModel.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -20,59 +20,59 @@ import java.util.Collections; import java.util.List; import java.util.Objects; -import java.util.function.Function; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.mybatis.dynamic.sql.SqlTable; -import org.mybatis.dynamic.sql.util.InsertMapping; +import org.mybatis.dynamic.sql.util.AbstractColumnMapping; public abstract class AbstractMultiRowInsertModel { - private SqlTable table; - private List records; - private List columnMappings; - + private final SqlTable table; + private final List records; + protected final List columnMappings; + protected AbstractMultiRowInsertModel(AbstractBuilder builder) { table = Objects.requireNonNull(builder.table); records = Collections.unmodifiableList(Objects.requireNonNull(builder.records)); columnMappings = Objects.requireNonNull(builder.columnMappings); } - public Stream mapColumnMappings(Function mapper) { - return columnMappings.stream().map(mapper); + public Stream columnMappings() { + return columnMappings.stream(); } - + public List records() { return records; } - + public SqlTable table() { return table; } - + public int recordCount() { return records.size(); } - + public abstract static class AbstractBuilder> { - private SqlTable table; - private List records = new ArrayList<>(); - private List columnMappings = new ArrayList<>(); - + private @Nullable SqlTable table; + private final List records = new ArrayList<>(); + private final List columnMappings = new ArrayList<>(); + public S withTable(SqlTable table) { this.table = table; return getThis(); } - + public S withRecords(Collection records) { this.records.addAll(records); return getThis(); } - - public S withColumnMappings(List columnMappings) { + + public S withColumnMappings(List columnMappings) { this.columnMappings.addAll(columnMappings); return getThis(); } - + protected abstract S getThis(); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/BatchInsertDSL.java b/src/main/java/org/mybatis/dynamic/sql/insert/BatchInsertDSL.java index 1622856a3..c2938e9c1 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/BatchInsertDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/BatchInsertDSL.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -19,30 +19,42 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Objects; +import org.jspecify.annotations.Nullable; import org.mybatis.dynamic.sql.SqlColumn; import org.mybatis.dynamic.sql.SqlTable; +import org.mybatis.dynamic.sql.util.AbstractColumnMapping; +import org.mybatis.dynamic.sql.util.Buildable; import org.mybatis.dynamic.sql.util.ConstantMapping; -import org.mybatis.dynamic.sql.util.InsertMapping; +import org.mybatis.dynamic.sql.util.MappedColumnMapping; import org.mybatis.dynamic.sql.util.NullMapping; import org.mybatis.dynamic.sql.util.PropertyMapping; +import org.mybatis.dynamic.sql.util.RowMapping; import org.mybatis.dynamic.sql.util.StringConstantMapping; -public class BatchInsertDSL { +public class BatchInsertDSL implements Buildable> { - private Collection records; - private SqlTable table; - private List columnMappings = new ArrayList<>(); - - private BatchInsertDSL(Collection records, SqlTable table) { - this.records = records; - this.table = table; + private final Collection records; + private final SqlTable table; + private final List columnMappings; + + private BatchInsertDSL(AbstractBuilder builder) { + this.records = builder.records; + this.table = Objects.requireNonNull(builder.table); + this.columnMappings = builder.columnMappings; } - + public ColumnMappingFinisher map(SqlColumn column) { return new ColumnMappingFinisher<>(column); } - + + public BatchInsertDSL withMappedColumn(SqlColumn column) { + columnMappings.add(MappedColumnMapping.of(column)); + return this; + } + + @Override public BatchInsertModel build() { return BatchInsertModel.withRecords(records) .withTable(table) @@ -51,51 +63,90 @@ public BatchInsertModel build() { } @SafeVarargs - public static IntoGatherer insert(T...records) { - return BatchInsertDSL.insert(Arrays.asList(records)); + public static BatchInsertDSL.IntoGatherer insert(T... records) { + return insert(Arrays.asList(records)); } - - public static IntoGatherer insert(Collection records) { + + public static BatchInsertDSL.IntoGatherer insert(Collection records) { return new IntoGatherer<>(records); } - + public static class IntoGatherer { - private Collection records; - + private final Collection records; + private IntoGatherer(Collection records) { this.records = records; } public BatchInsertDSL into(SqlTable table) { - return new BatchInsertDSL<>(records, table); + return new Builder().withRecords(records).withTable(table).build(); } } - + public class ColumnMappingFinisher { - private SqlColumn column; - + private final SqlColumn column; + public ColumnMappingFinisher(SqlColumn column) { this.column = column; } - + public BatchInsertDSL toProperty(String property) { columnMappings.add(PropertyMapping.of(column, property)); return BatchInsertDSL.this; } - + public BatchInsertDSL toNull() { columnMappings.add(NullMapping.of(column)); return BatchInsertDSL.this; } - + public BatchInsertDSL toConstant(String constant) { columnMappings.add(ConstantMapping.of(column, constant)); return BatchInsertDSL.this; } - + public BatchInsertDSL toStringConstant(String constant) { columnMappings.add(StringConstantMapping.of(column, constant)); return BatchInsertDSL.this; } + + public BatchInsertDSL toRow() { + columnMappings.add(RowMapping.of(column)); + return BatchInsertDSL.this; + } + } + + public abstract static class AbstractBuilder> { + final Collection records = new ArrayList<>(); + @Nullable SqlTable table; + final List columnMappings = new ArrayList<>(); + + public B withRecords(Collection records) { + this.records.addAll(records); + return getThis(); + } + + public B withTable(SqlTable table) { + this.table = table; + return getThis(); + } + + public B withColumnMappings(Collection columnMappings) { + this.columnMappings.addAll(columnMappings); + return getThis(); + } + + protected abstract B getThis(); + } + + public static class Builder extends AbstractBuilder> { + @Override + protected Builder getThis() { + return this; + } + + public BatchInsertDSL build() { + return new BatchInsertDSL<>(this); + } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/BatchInsertModel.java b/src/main/java/org/mybatis/dynamic/sql/insert/BatchInsertModel.java index d8c259612..275ce2745 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/BatchInsertModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/BatchInsertModel.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -20,11 +20,14 @@ import org.mybatis.dynamic.sql.insert.render.BatchInsert; import org.mybatis.dynamic.sql.insert.render.BatchInsertRenderer; import org.mybatis.dynamic.sql.render.RenderingStrategy; +import org.mybatis.dynamic.sql.util.Validator; public class BatchInsertModel extends AbstractMultiRowInsertModel { - + private BatchInsertModel(Builder builder) { super(builder); + Validator.assertNotEmpty(records(), "ERROR.19"); //$NON-NLS-1$ + Validator.assertNotEmpty(columnMappings, "ERROR.5"); //$NON-NLS-1$ } public BatchInsert render(RenderingStrategy renderingStrategy) { @@ -33,17 +36,17 @@ public BatchInsert render(RenderingStrategy renderingStrategy) { .build() .render(); } - + public static Builder withRecords(Collection records) { return new Builder().withRecords(records); } - + public static class Builder extends AbstractBuilder> { @Override protected Builder getThis() { return this; } - + public BatchInsertModel build() { return new BatchInsertModel<>(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/GeneralInsertDSL.java b/src/main/java/org/mybatis/dynamic/sql/insert/GeneralInsertDSL.java new file mode 100644 index 000000000..5cba9063d --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/insert/GeneralInsertDSL.java @@ -0,0 +1,132 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.insert; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.function.Supplier; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.SqlColumn; +import org.mybatis.dynamic.sql.SqlTable; +import org.mybatis.dynamic.sql.configuration.StatementConfiguration; +import org.mybatis.dynamic.sql.util.AbstractColumnMapping; +import org.mybatis.dynamic.sql.util.Buildable; +import org.mybatis.dynamic.sql.util.ConstantMapping; +import org.mybatis.dynamic.sql.util.NullMapping; +import org.mybatis.dynamic.sql.util.StringConstantMapping; +import org.mybatis.dynamic.sql.util.ValueMapping; +import org.mybatis.dynamic.sql.util.ValueOrNullMapping; +import org.mybatis.dynamic.sql.util.ValueWhenPresentMapping; + +public class GeneralInsertDSL implements Buildable { + private final List columnMappings; + private final SqlTable table; + + private GeneralInsertDSL(Builder builder) { + table = Objects.requireNonNull(builder.table); + columnMappings = builder.columnMappings; + } + + public SetClauseFinisher set(SqlColumn column) { + return new SetClauseFinisher<>(column); + } + + @Override + public GeneralInsertModel build() { + return new GeneralInsertModel.Builder() + .withTable(table) + .withInsertMappings(columnMappings) + .withStatementConfiguration(new StatementConfiguration()) // nothing configurable in this statement yet + .build(); + } + + public static GeneralInsertDSL insertInto(SqlTable table) { + return new GeneralInsertDSL.Builder().withTable(table).build(); + } + + public class SetClauseFinisher { + + private final SqlColumn column; + + public SetClauseFinisher(SqlColumn column) { + this.column = column; + } + + public GeneralInsertDSL toNull() { + columnMappings.add(NullMapping.of(column)); + return GeneralInsertDSL.this; + } + + public GeneralInsertDSL toConstant(String constant) { + columnMappings.add(ConstantMapping.of(column, constant)); + return GeneralInsertDSL.this; + } + + public GeneralInsertDSL toStringConstant(String constant) { + columnMappings.add(StringConstantMapping.of(column, constant)); + return GeneralInsertDSL.this; + } + + public GeneralInsertDSL toValue(T value) { + return toValue(() -> value); + } + + public GeneralInsertDSL toValue(Supplier valueSupplier) { + columnMappings.add(ValueMapping.of(column, valueSupplier)); + return GeneralInsertDSL.this; + } + + public GeneralInsertDSL toValueOrNull(@Nullable T value) { + return toValueOrNull(() -> value); + } + + public GeneralInsertDSL toValueOrNull(Supplier<@Nullable T> valueSupplier) { + columnMappings.add(ValueOrNullMapping.of(column, valueSupplier)); + return GeneralInsertDSL.this; + } + + public GeneralInsertDSL toValueWhenPresent(@Nullable T value) { + return toValueWhenPresent(() -> value); + } + + public GeneralInsertDSL toValueWhenPresent(Supplier<@Nullable T> valueSupplier) { + columnMappings.add(ValueWhenPresentMapping.of(column, valueSupplier)); + return GeneralInsertDSL.this; + } + } + + public static class Builder { + private final List columnMappings = new ArrayList<>(); + private @Nullable SqlTable table; + + public Builder withTable(SqlTable table) { + this.table = table; + return this; + } + + public Builder withColumnMappings(Collection columnMappings) { + this.columnMappings.addAll(columnMappings); + return this; + } + + public GeneralInsertDSL build() { + return new GeneralInsertDSL(this); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/GeneralInsertModel.java b/src/main/java/org/mybatis/dynamic/sql/insert/GeneralInsertModel.java new file mode 100644 index 000000000..873c98f5f --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/insert/GeneralInsertModel.java @@ -0,0 +1,88 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.insert; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.SqlTable; +import org.mybatis.dynamic.sql.configuration.StatementConfiguration; +import org.mybatis.dynamic.sql.insert.render.GeneralInsertRenderer; +import org.mybatis.dynamic.sql.insert.render.GeneralInsertStatementProvider; +import org.mybatis.dynamic.sql.render.RenderingStrategy; +import org.mybatis.dynamic.sql.util.AbstractColumnMapping; +import org.mybatis.dynamic.sql.util.Validator; + +public class GeneralInsertModel { + + private final SqlTable table; + private final List insertMappings; + private final StatementConfiguration statementConfiguration; + + private GeneralInsertModel(Builder builder) { + table = Objects.requireNonNull(builder.table); + Validator.assertNotEmpty(builder.insertMappings, "ERROR.6"); //$NON-NLS-1$ + insertMappings = builder.insertMappings; + statementConfiguration = Objects.requireNonNull(builder.statementConfiguration); + } + + public Stream columnMappings() { + return insertMappings.stream(); + } + + public SqlTable table() { + return table; + } + + public StatementConfiguration statementConfiguration() { + return statementConfiguration; + } + + public GeneralInsertStatementProvider render(RenderingStrategy renderingStrategy) { + return GeneralInsertRenderer.withInsertModel(this) + .withRenderingStrategy(renderingStrategy) + .build() + .render(); + } + + public static class Builder { + private @Nullable SqlTable table; + private final List insertMappings = new ArrayList<>(); + private @Nullable StatementConfiguration statementConfiguration; + + public Builder withTable(SqlTable table) { + this.table = table; + return this; + } + + public Builder withInsertMappings(List insertMappings) { + this.insertMappings.addAll(insertMappings); + return this; + } + + public Builder withStatementConfiguration(StatementConfiguration statementConfiguration) { + this.statementConfiguration = statementConfiguration; + return this; + } + + public GeneralInsertModel build() { + return new GeneralInsertModel(this); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/InsertColumnListModel.java b/src/main/java/org/mybatis/dynamic/sql/insert/InsertColumnListModel.java index c16eae68d..56131f0e9 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/InsertColumnListModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/InsertColumnListModel.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -17,23 +17,28 @@ import java.util.ArrayList; import java.util.List; -import java.util.function.Function; +import java.util.Objects; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.mybatis.dynamic.sql.SqlColumn; +import org.mybatis.dynamic.sql.util.Validator; public class InsertColumnListModel { - private List> columns = new ArrayList<>(); - - private InsertColumnListModel(List> columns) { + private final List> columns = new ArrayList<>(); + + private InsertColumnListModel(@Nullable List> columns) { + Objects.requireNonNull(columns); + Validator.assertNotEmpty(columns, "ERROR.4"); //$NON-NLS-1$ this.columns.addAll(columns); } - public Stream mapColumns(Function, R> mapper) { - return columns.stream().map(mapper); + @SuppressWarnings("java:S1452") + public Stream> columns() { + return columns.stream(); } - - public static InsertColumnListModel of(List> columns) { + + public static InsertColumnListModel of(@Nullable List> columns) { return new InsertColumnListModel(columns); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/InsertDSL.java b/src/main/java/org/mybatis/dynamic/sql/insert/InsertDSL.java index f8f329cae..8b95a9c0f 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/InsertDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/InsertDSL.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -16,87 +16,135 @@ package org.mybatis.dynamic.sql.insert; import java.util.ArrayList; +import java.util.Collection; import java.util.List; +import java.util.Objects; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; import org.mybatis.dynamic.sql.SqlColumn; import org.mybatis.dynamic.sql.SqlTable; +import org.mybatis.dynamic.sql.util.AbstractColumnMapping; +import org.mybatis.dynamic.sql.util.Buildable; import org.mybatis.dynamic.sql.util.ConstantMapping; -import org.mybatis.dynamic.sql.util.InsertMapping; +import org.mybatis.dynamic.sql.util.MappedColumnMapping; +import org.mybatis.dynamic.sql.util.MappedColumnWhenPresentMapping; import org.mybatis.dynamic.sql.util.NullMapping; import org.mybatis.dynamic.sql.util.PropertyMapping; +import org.mybatis.dynamic.sql.util.PropertyWhenPresentMapping; +import org.mybatis.dynamic.sql.util.RowMapping; import org.mybatis.dynamic.sql.util.StringConstantMapping; -public class InsertDSL { +public class InsertDSL implements Buildable> { - private T record; - private SqlTable table; - private List columnMappings = new ArrayList<>(); - - private InsertDSL(T record, SqlTable table) { - this.record = record; - this.table = table; + private final T row; + private final SqlTable table; + private final List columnMappings; + + private InsertDSL(Builder builder) { + this.row = Objects.requireNonNull(builder.row); + this.table = Objects.requireNonNull(builder.table); + columnMappings = builder.columnMappings; } - + public ColumnMappingFinisher map(SqlColumn column) { return new ColumnMappingFinisher<>(column); } - + + public InsertDSL withMappedColumn(SqlColumn column) { + columnMappings.add(MappedColumnMapping.of(column)); + return this; + } + + public InsertDSL withMappedColumnWhenPresent(SqlColumn column, Supplier valueSupplier) { + columnMappings.add(MappedColumnWhenPresentMapping.of(column, valueSupplier)); + return this; + } + + @Override public InsertModel build() { - return InsertModel.withRecord(record) + return InsertModel.withRow(row) .withTable(table) .withColumnMappings(columnMappings) .build(); } - - public static IntoGatherer insert(T record) { - return new IntoGatherer<>(record); + + public static IntoGatherer insert(T row) { + return new IntoGatherer<>(row); } public static class IntoGatherer { - private T record; - - private IntoGatherer(T record) { - this.record = record; + private final T row; + + private IntoGatherer(T row) { + this.row = row; } public InsertDSL into(SqlTable table) { - return new InsertDSL<>(record, table); + return new InsertDSL.Builder().withRow(row).withTable(table).build(); } } - + public class ColumnMappingFinisher { - private SqlColumn column; - + private final SqlColumn column; + public ColumnMappingFinisher(SqlColumn column) { this.column = column; } - + public InsertDSL toProperty(String property) { columnMappings.add(PropertyMapping.of(column, property)); return InsertDSL.this; } - + public InsertDSL toPropertyWhenPresent(String property, Supplier valueSupplier) { - if (valueSupplier.get() != null) { - toProperty(property); - } + columnMappings.add(PropertyWhenPresentMapping.of(column, property, valueSupplier)); return InsertDSL.this; } - + public InsertDSL toNull() { columnMappings.add(NullMapping.of(column)); return InsertDSL.this; } - + public InsertDSL toConstant(String constant) { columnMappings.add(ConstantMapping.of(column, constant)); return InsertDSL.this; } - + public InsertDSL toStringConstant(String constant) { columnMappings.add(StringConstantMapping.of(column, constant)); return InsertDSL.this; } + + public InsertDSL toRow() { + columnMappings.add(RowMapping.of(column)); + return InsertDSL.this; + } + } + + public static class Builder { + private @Nullable T row; + private @Nullable SqlTable table; + private final List columnMappings = new ArrayList<>(); + + public Builder withRow(T row) { + this.row = row; + return this; + } + + public Builder withTable(SqlTable table) { + this.table = table; + return this; + } + + public Builder withColumnMappings(Collection columnMappings) { + this.columnMappings.addAll(columnMappings); + return this; + } + + public InsertDSL build() { + return new InsertDSL<>(this); + } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/InsertModel.java b/src/main/java/org/mybatis/dynamic/sql/insert/InsertModel.java index 7f4c9bca8..4495e32fd 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/InsertModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/InsertModel.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -18,69 +18,71 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.function.Function; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.mybatis.dynamic.sql.SqlTable; import org.mybatis.dynamic.sql.insert.render.InsertRenderer; import org.mybatis.dynamic.sql.insert.render.InsertStatementProvider; import org.mybatis.dynamic.sql.render.RenderingStrategy; -import org.mybatis.dynamic.sql.util.InsertMapping; +import org.mybatis.dynamic.sql.util.AbstractColumnMapping; +import org.mybatis.dynamic.sql.util.Validator; public class InsertModel { - private SqlTable table; - private T record; - private List columnMappings; - + private final SqlTable table; + private final T row; + private final List columnMappings; + private InsertModel(Builder builder) { table = Objects.requireNonNull(builder.table); - record = Objects.requireNonNull(builder.record); + row = Objects.requireNonNull(builder.row); columnMappings = Objects.requireNonNull(builder.columnMappings); + Validator.assertNotEmpty(columnMappings, "ERROR.7"); //$NON-NLS-1$ } - public Stream mapColumnMappings(Function mapper) { - return columnMappings.stream().map(mapper); + public Stream columnMappings() { + return columnMappings.stream(); } - - public T record() { - return record; + + public T row() { + return row; } - + public SqlTable table() { return table; } - + public InsertStatementProvider render(RenderingStrategy renderingStrategy) { return InsertRenderer.withInsertModel(this) .withRenderingStrategy(renderingStrategy) .build() .render(); } - - public static Builder withRecord(T record) { - return new Builder().withRecord(record); + + public static Builder withRow(T row) { + return new Builder().withRow(row); } - + public static class Builder { - private SqlTable table; - private T record; - private List columnMappings = new ArrayList<>(); - + private @Nullable SqlTable table; + private @Nullable T row; + private final List columnMappings = new ArrayList<>(); + public Builder withTable(SqlTable table) { this.table = table; return this; } - - public Builder withRecord(T record) { - this.record = record; + + public Builder withRow(T row) { + this.row = row; return this; } - - public Builder withColumnMappings(List columnMappings) { + + public Builder withColumnMappings(List columnMappings) { this.columnMappings.addAll(columnMappings); return this; } - + public InsertModel build() { return new InsertModel<>(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/InsertSelectDSL.java b/src/main/java/org/mybatis/dynamic/sql/insert/InsertSelectDSL.java index 1441cfbca..e7f8b5e09 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/InsertSelectDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/InsertSelectDSL.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -18,64 +18,83 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; import org.mybatis.dynamic.sql.SqlColumn; import org.mybatis.dynamic.sql.SqlTable; +import org.mybatis.dynamic.sql.configuration.StatementConfiguration; import org.mybatis.dynamic.sql.select.SelectModel; import org.mybatis.dynamic.sql.util.Buildable; +import org.mybatis.dynamic.sql.util.ConfigurableStatement; -public class InsertSelectDSL { +public class InsertSelectDSL implements Buildable, ConfigurableStatement { + + private final SqlTable table; + private final @Nullable InsertColumnListModel columnList; + private final SelectModel selectModel; + private final StatementConfiguration statementConfiguration = new StatementConfiguration(); - private SqlTable table; - private InsertColumnListModel columnList; - private SelectModel selectModel; - private InsertSelectDSL(SqlTable table, InsertColumnListModel columnList, SelectModel selectModel) { - this(table, selectModel); + this.table = Objects.requireNonNull(table); + this.selectModel = Objects.requireNonNull(selectModel); this.columnList = columnList; } - + private InsertSelectDSL(SqlTable table, SelectModel selectModel) { this.table = Objects.requireNonNull(table); this.selectModel = Objects.requireNonNull(selectModel); + this.columnList = null; } - + + @Override public InsertSelectModel build() { return InsertSelectModel.withTable(table) .withColumnList(columnList) .withSelectModel(selectModel) + .withStatementConfiguration(statementConfiguration) .build(); } - + public static InsertColumnGatherer insertInto(SqlTable table) { return new InsertColumnGatherer(table); } + @Override + public InsertSelectDSL configureStatement(Consumer consumer) { + consumer.accept(statementConfiguration); + return this; + } + public static class InsertColumnGatherer { - private SqlTable table; - + private final SqlTable table; + private InsertColumnGatherer(SqlTable table) { this.table = table; } - - public SelectGatherer withColumnList(SqlColumn...columns) { - return new SelectGatherer(table, Arrays.asList(columns)); + + public SelectGatherer withColumnList(SqlColumn... columns) { + return withColumnList(Arrays.asList(columns)); + } + + public SelectGatherer withColumnList(List> columns) { + return new SelectGatherer(table, columns); } public InsertSelectDSL withSelectStatement(Buildable selectModelBuilder) { return new InsertSelectDSL(table, selectModelBuilder.build()); } } - + public static class SelectGatherer { - private SqlTable table; - private InsertColumnListModel columnList; - + private final SqlTable table; + private final InsertColumnListModel columnList; + private SelectGatherer(SqlTable table, List> columns) { this.table = table; columnList = InsertColumnListModel.of(columns); } - + public InsertSelectDSL withSelectStatement(Buildable selectModelBuilder) { return new InsertSelectDSL(table, columnList, selectModelBuilder.build()); } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/InsertSelectModel.java b/src/main/java/org/mybatis/dynamic/sql/insert/InsertSelectModel.java index ef875605e..36051700c 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/InsertSelectModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/InsertSelectModel.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -18,66 +18,80 @@ import java.util.Objects; import java.util.Optional; +import org.jspecify.annotations.Nullable; import org.mybatis.dynamic.sql.SqlTable; +import org.mybatis.dynamic.sql.configuration.StatementConfiguration; import org.mybatis.dynamic.sql.insert.render.InsertSelectRenderer; import org.mybatis.dynamic.sql.insert.render.InsertSelectStatementProvider; import org.mybatis.dynamic.sql.render.RenderingStrategy; import org.mybatis.dynamic.sql.select.SelectModel; public class InsertSelectModel { - private SqlTable table; - private InsertColumnListModel columnList; - private SelectModel selectModel; - + private final SqlTable table; + private final @Nullable InsertColumnListModel columnList; + private final SelectModel selectModel; + private final StatementConfiguration statementConfiguration; + private InsertSelectModel(Builder builder) { table = Objects.requireNonNull(builder.table); columnList = builder.columnList; selectModel = Objects.requireNonNull(builder.selectModel); + statementConfiguration = Objects.requireNonNull(builder.statementConfiguration); } public SqlTable table() { return table; } - + public SelectModel selectModel() { return selectModel; } - + public Optional columnList() { return Optional.ofNullable(columnList); } - + + public StatementConfiguration statementConfiguration() { + return statementConfiguration; + } + public InsertSelectStatementProvider render(RenderingStrategy renderingStrategy) { return InsertSelectRenderer.withInsertSelectModel(this) .withRenderingStrategy(renderingStrategy) .build() .render(); } - + public static Builder withTable(SqlTable table) { return new Builder().withTable(table); } - + public static class Builder { - private SqlTable table; - private InsertColumnListModel columnList; - private SelectModel selectModel; - + private @Nullable SqlTable table; + private @Nullable InsertColumnListModel columnList; + private @Nullable SelectModel selectModel; + private @Nullable StatementConfiguration statementConfiguration; + public Builder withTable(SqlTable table) { this.table = table; return this; } - - public Builder withColumnList(InsertColumnListModel columnList) { + + public Builder withColumnList(@Nullable InsertColumnListModel columnList) { this.columnList = columnList; return this; } - + public Builder withSelectModel(SelectModel selectModel) { this.selectModel = selectModel; return this; } - + + public Builder withStatementConfiguration(StatementConfiguration statementConfiguration) { + this.statementConfiguration = statementConfiguration; + return this; + } + public InsertSelectModel build() { return new InsertSelectModel(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/MultiRowInsertDSL.java b/src/main/java/org/mybatis/dynamic/sql/insert/MultiRowInsertDSL.java index 8b80bd913..87705c415 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/MultiRowInsertDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/MultiRowInsertDSL.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,34 +15,44 @@ */ package org.mybatis.dynamic.sql.insert; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Objects; import org.mybatis.dynamic.sql.SqlColumn; import org.mybatis.dynamic.sql.SqlTable; +import org.mybatis.dynamic.sql.util.AbstractColumnMapping; +import org.mybatis.dynamic.sql.util.Buildable; import org.mybatis.dynamic.sql.util.ConstantMapping; -import org.mybatis.dynamic.sql.util.InsertMapping; +import org.mybatis.dynamic.sql.util.MappedColumnMapping; import org.mybatis.dynamic.sql.util.NullMapping; import org.mybatis.dynamic.sql.util.PropertyMapping; +import org.mybatis.dynamic.sql.util.RowMapping; import org.mybatis.dynamic.sql.util.StringConstantMapping; -public class MultiRowInsertDSL { +public class MultiRowInsertDSL implements Buildable> { - private Collection records; - private SqlTable table; - private List columnMappings = new ArrayList<>(); - - private MultiRowInsertDSL(Collection records, SqlTable table) { - this.records = records; - this.table = table; + private final Collection records; + private final SqlTable table; + private final List columnMappings; + + private MultiRowInsertDSL(BatchInsertDSL.AbstractBuilder builder) { + this.records = builder.records; + this.table = Objects.requireNonNull(builder.table); + this.columnMappings = builder.columnMappings; } - + public ColumnMappingFinisher map(SqlColumn column) { return new ColumnMappingFinisher<>(column); } - + + public MultiRowInsertDSL withMappedColumn(SqlColumn column) { + columnMappings.add(MappedColumnMapping.of(column)); + return this; + } + + @Override public MultiRowInsertModel build() { return MultiRowInsertModel.withRecords(records) .withTable(table) @@ -51,51 +61,68 @@ public MultiRowInsertModel build() { } @SafeVarargs - public static IntoGatherer insert(T...records) { - return MultiRowInsertDSL.insert(Arrays.asList(records)); + public static MultiRowInsertDSL.IntoGatherer insert(T... records) { + return insert(Arrays.asList(records)); } - - public static IntoGatherer insert(Collection records) { + + public static MultiRowInsertDSL.IntoGatherer insert(Collection records) { return new IntoGatherer<>(records); } - + public static class IntoGatherer { - private Collection records; - + private final Collection records; + private IntoGatherer(Collection records) { this.records = records; } public MultiRowInsertDSL into(SqlTable table) { - return new MultiRowInsertDSL<>(records, table); + return new Builder().withRecords(records).withTable(table).build(); } } - + public class ColumnMappingFinisher { - private SqlColumn column; - + private final SqlColumn column; + public ColumnMappingFinisher(SqlColumn column) { this.column = column; } - + public MultiRowInsertDSL toProperty(String property) { columnMappings.add(PropertyMapping.of(column, property)); return MultiRowInsertDSL.this; } - + public MultiRowInsertDSL toNull() { columnMappings.add(NullMapping.of(column)); return MultiRowInsertDSL.this; } - + public MultiRowInsertDSL toConstant(String constant) { columnMappings.add(ConstantMapping.of(column, constant)); return MultiRowInsertDSL.this; } - + public MultiRowInsertDSL toStringConstant(String constant) { columnMappings.add(StringConstantMapping.of(column, constant)); return MultiRowInsertDSL.this; } + + public MultiRowInsertDSL toRow() { + columnMappings.add(RowMapping.of(column)); + return MultiRowInsertDSL.this; + } + } + + public static class Builder extends BatchInsertDSL.AbstractBuilder> { + + @Override + protected Builder getThis() { + return this; + } + + public MultiRowInsertDSL build() { + return new MultiRowInsertDSL<>(this); + } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/MultiRowInsertModel.java b/src/main/java/org/mybatis/dynamic/sql/insert/MultiRowInsertModel.java index d26c57c3d..435267652 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/MultiRowInsertModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/MultiRowInsertModel.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -20,24 +20,27 @@ import org.mybatis.dynamic.sql.insert.render.MultiRowInsertRenderer; import org.mybatis.dynamic.sql.insert.render.MultiRowInsertStatementProvider; import org.mybatis.dynamic.sql.render.RenderingStrategy; +import org.mybatis.dynamic.sql.util.Validator; public class MultiRowInsertModel extends AbstractMultiRowInsertModel { - + private MultiRowInsertModel(Builder builder) { super(builder); + Validator.assertNotEmpty(records(), "ERROR.20"); //$NON-NLS-1$ + Validator.assertNotEmpty(columnMappings, "ERROR.8"); //$NON-NLS-1$ } - + public MultiRowInsertStatementProvider render(RenderingStrategy renderingStrategy) { return MultiRowInsertRenderer.withMultiRowInsertModel(this) .withRenderingStrategy(renderingStrategy) .build() .render(); } - + public static Builder withRecords(Collection records) { return new Builder().withRecords(records); } - + public static class Builder extends AbstractBuilder> { @Override protected Builder getThis() { diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/package-info.java b/src/main/java/org/mybatis/dynamic/sql/insert/package-info.java new file mode 100644 index 000000000..5ef9f74ea --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/insert/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +@NullMarked +package org.mybatis.dynamic.sql.insert; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/BatchInsert.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/BatchInsert.java index d3344b999..8af9e30fe 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/render/BatchInsert.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/BatchInsert.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -19,51 +19,56 @@ import java.util.Collections; import java.util.List; import java.util.Objects; -import java.util.stream.Collectors; + +import org.jspecify.annotations.Nullable; public class BatchInsert { - private String insertStatement; - private List records; - + private final String insertStatement; + private final List records; + private BatchInsert(Builder builder) { insertStatement = Objects.requireNonNull(builder.insertStatement); records = Collections.unmodifiableList(Objects.requireNonNull(builder.records)); } - + /** - * Returns a list of InsertStatement objects. This is useful for MyBatis batch support. - * + * Returns a list of InsertStatement objects. This is useful for MyBatis batch support. + * * @return a List of InsertStatements */ public List> insertStatements() { return records.stream() .map(this::toInsertStatement) - .collect(Collectors.toList()); + .toList(); } - - private InsertStatementProvider toInsertStatement(T record) { - return DefaultInsertStatementProvider.withRecord(record) + + private InsertStatementProvider toInsertStatement(T row) { + return DefaultInsertStatementProvider.withRow(row) .withInsertStatement(insertStatement) .build(); } /** - * Returns the generated SQL for this batch. This is useful for Spring JDBC batch support. - * + * Returns the generated SQL for this batch. This is useful for Spring JDBC batch support. + * * @return the generated INSERT statement */ public String getInsertStatementSQL() { return insertStatement; } - + + public List getRecords() { + return records; + } + public static Builder withRecords(List records) { return new Builder().withRecords(records); } - + public static class Builder { - private String insertStatement; - private List records = new ArrayList<>(); - + private @Nullable String insertStatement; + private final List records = new ArrayList<>(); + public Builder withInsertStatement(String insertStatement) { this.insertStatement = insertStatement; return this; @@ -73,7 +78,7 @@ public Builder withRecords(List records) { this.records.addAll(records); return this; } - + public BatchInsert build() { return new BatchInsert<>(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/BatchInsertRenderer.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/BatchInsertRenderer.java index 25f78a2f8..875820c02 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/render/BatchInsertRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/BatchInsertRenderer.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,58 +15,53 @@ */ package org.mybatis.dynamic.sql.insert.render; -import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore; - import java.util.Objects; +import org.jspecify.annotations.Nullable; import org.mybatis.dynamic.sql.insert.BatchInsertModel; import org.mybatis.dynamic.sql.render.RenderingStrategy; public class BatchInsertRenderer { - private BatchInsertModel model; - private RenderingStrategy renderingStrategy; - + private final BatchInsertModel model; + private final MultiRowValuePhraseVisitor visitor; + private BatchInsertRenderer(Builder builder) { model = Objects.requireNonNull(builder.model); - renderingStrategy = Objects.requireNonNull(builder.renderingStrategy); + visitor = new MultiRowValuePhraseVisitor(Objects.requireNonNull(builder.renderingStrategy), + "row"); //$NON-NLS-1$) } - + public BatchInsert render() { - ValuePhraseVisitor visitor = new ValuePhraseVisitor(renderingStrategy); - FieldAndValueCollector collector = model.mapColumnMappings(MultiRowRenderingUtilities.toFieldAndValue(visitor)) + FieldAndValueCollector collector = model.columnMappings() + .map(m -> m.accept(visitor)) .collect(FieldAndValueCollector.collect()); - + + String insertStatement = InsertRenderingUtilities.calculateInsertStatement(model.table(), collector); + return BatchInsert.withRecords(model.records()) - .withInsertStatement(calculateInsertStatement(collector)) + .withInsertStatement(insertStatement) .build(); } - - private String calculateInsertStatement(FieldAndValueCollector collector) { - return "insert into" //$NON-NLS-1$ - + spaceBefore(model.table().tableNameAtRuntime()) - + spaceBefore(collector.columnsPhrase()) - + spaceBefore(collector.valuesPhrase()); - } - + public static Builder withBatchInsertModel(BatchInsertModel model) { return new Builder().withBatchInsertModel(model); } - + public static class Builder { - private BatchInsertModel model; - private RenderingStrategy renderingStrategy; - + private @Nullable BatchInsertModel model; + private @Nullable RenderingStrategy renderingStrategy; + public Builder withBatchInsertModel(BatchInsertModel model) { this.model = model; return this; } - + public Builder withRenderingStrategy(RenderingStrategy renderingStrategy) { this.renderingStrategy = renderingStrategy; return this; } - + public BatchInsertRenderer build() { return new BatchInsertRenderer<>(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/DefaultInsertSelectStatementProvider.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/DefaultGeneralInsertStatementProvider.java similarity index 65% rename from src/main/java/org/mybatis/dynamic/sql/insert/render/DefaultInsertSelectStatementProvider.java rename to src/main/java/org/mybatis/dynamic/sql/insert/render/DefaultGeneralInsertStatementProvider.java index f647a8160..eaa3d0911 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/render/DefaultInsertSelectStatementProvider.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/DefaultGeneralInsertStatementProvider.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -19,33 +19,36 @@ import java.util.Map; import java.util.Objects; -public class DefaultInsertSelectStatementProvider implements InsertSelectStatementProvider { - private String insertStatement; - private Map parameters; - - private DefaultInsertSelectStatementProvider(Builder builder) { +import org.jspecify.annotations.Nullable; + +public class DefaultGeneralInsertStatementProvider + implements GeneralInsertStatementProvider, InsertSelectStatementProvider { + private final String insertStatement; + private final Map parameters; + + private DefaultGeneralInsertStatementProvider(Builder builder) { insertStatement = Objects.requireNonNull(builder.insertStatement); - parameters = Objects.requireNonNull(builder.parameters); - } - - @Override - public String getInsertStatement() { - return insertStatement; + parameters = builder.parameters; } - + @Override public Map getParameters() { return parameters; } + @Override + public String getInsertStatement() { + return insertStatement; + } + public static Builder withInsertStatement(String insertStatement) { return new Builder().withInsertStatement(insertStatement); } - + public static class Builder { - private String insertStatement; - private Map parameters = new HashMap<>(); - + private @Nullable String insertStatement; + private final Map parameters = new HashMap<>(); + public Builder withInsertStatement(String insertStatement) { this.insertStatement = insertStatement; return this; @@ -55,9 +58,9 @@ public Builder withParameters(Map parameters) { this.parameters.putAll(parameters); return this; } - - public DefaultInsertSelectStatementProvider build() { - return new DefaultInsertSelectStatementProvider(this); + + public DefaultGeneralInsertStatementProvider build() { + return new DefaultGeneralInsertStatementProvider(this); } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/DefaultInsertStatementProvider.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/DefaultInsertStatementProvider.java index b75e59e2a..a16967456 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/render/DefaultInsertStatementProvider.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/DefaultInsertStatementProvider.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -17,43 +17,45 @@ import java.util.Objects; +import org.jspecify.annotations.Nullable; + public class DefaultInsertStatementProvider implements InsertStatementProvider { - private String insertStatement; - private T record; - + private final String insertStatement; + private final T row; + private DefaultInsertStatementProvider(Builder builder) { insertStatement = Objects.requireNonNull(builder.insertStatement); - record = Objects.requireNonNull(builder.record); + row = Objects.requireNonNull(builder.row); } - + @Override - public T getRecord() { - return record; + public T getRow() { + return row; } - + @Override public String getInsertStatement() { return insertStatement; } - public static Builder withRecord(T record) { - return new Builder().withRecord(record); + public static Builder withRow(T row) { + return new Builder().withRow(row); } - + public static class Builder { - private String insertStatement; - private T record; - + private @Nullable String insertStatement; + private @Nullable T row; + public Builder withInsertStatement(String insertStatement) { this.insertStatement = insertStatement; return this; } - public Builder withRecord(T record) { - this.record = record; + public Builder withRow(T row) { + this.row = row; return this; } - + public DefaultInsertStatementProvider build() { return new DefaultInsertStatementProvider<>(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/DefaultMultiRowInsertStatementProvider.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/DefaultMultiRowInsertStatementProvider.java index 67966ee2a..db8c9f489 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/render/DefaultMultiRowInsertStatementProvider.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/DefaultMultiRowInsertStatementProvider.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -20,40 +20,42 @@ import java.util.List; import java.util.Objects; +import org.jspecify.annotations.Nullable; + public class DefaultMultiRowInsertStatementProvider implements MultiRowInsertStatementProvider { - - private List records; - private String insertStatement; - + + private final List records; + private final String insertStatement; + private DefaultMultiRowInsertStatementProvider(Builder builder) { insertStatement = Objects.requireNonNull(builder.insertStatement); records = Collections.unmodifiableList(builder.records); } - + @Override public String getInsertStatement() { return insertStatement; } - + @Override public List getRecords() { return records; } - + public static class Builder { - private List records = new ArrayList<>(); - private String insertStatement; + private final List records = new ArrayList<>(); + private @Nullable String insertStatement; public Builder withRecords(List records) { this.records.addAll(records); return this; } - + public Builder withInsertStatement(String insertStatement) { this.insertStatement = insertStatement; return this; } - + public DefaultMultiRowInsertStatementProvider build() { return new DefaultMultiRowInsertStatementProvider<>(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/FieldAndValue.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/FieldAndValueAndParameters.java similarity index 51% rename from src/main/java/org/mybatis/dynamic/sql/insert/render/FieldAndValue.java rename to src/main/java/org/mybatis/dynamic/sql/insert/render/FieldAndValueAndParameters.java index 22473d66a..d923020c0 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/render/FieldAndValue.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/FieldAndValueAndParameters.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,49 +15,69 @@ */ package org.mybatis.dynamic.sql.insert.render; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; +import java.util.Optional; -public class FieldAndValue { - private String fieldName; - private String valuePhrase; - - private FieldAndValue(Builder builder) { +import org.jspecify.annotations.Nullable; + +public class FieldAndValueAndParameters { + private final String fieldName; + private final String valuePhrase; + private final Map parameters; + + private FieldAndValueAndParameters(Builder builder) { fieldName = Objects.requireNonNull(builder.fieldName); valuePhrase = Objects.requireNonNull(builder.valuePhrase); + parameters = builder.parameters; } - + public String fieldName() { return fieldName; } - + public String valuePhrase() { return valuePhrase; } - - public String valuePhrase(int row) { - return String.format(valuePhrase, row); + + public Map parameters() { + return parameters; } public static Builder withFieldName(String fieldName) { return new Builder().withFieldName(fieldName); } - + public static class Builder { - private String fieldName; - private String valuePhrase; + private @Nullable String fieldName; + private @Nullable String valuePhrase; + private final Map parameters = new HashMap<>(); public Builder withFieldName(String fieldName) { this.fieldName = fieldName; return this; } - + public Builder withValuePhrase(String valuePhrase) { this.valuePhrase = valuePhrase; return this; } - - public FieldAndValue build() { - return new FieldAndValue(this); + + public Builder withParameter(String key, @Nullable Object value) { + // the value can be null because a parameter type converter may return null + + //noinspection DataFlowIssue + parameters.put(key, value); + return this; + } + + public FieldAndValueAndParameters build() { + return new FieldAndValueAndParameters(this); + } + + public Optional buildOptional() { + return Optional.of(build()); } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/FieldAndValueCollector.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/FieldAndValueCollector.java index cc006d4f3..7af466766 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/render/FieldAndValueCollector.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/FieldAndValueCollector.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -16,55 +16,67 @@ package org.mybatis.dynamic.sql.insert.render; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collector; import java.util.stream.Collectors; import java.util.stream.IntStream; public class FieldAndValueCollector { - - private List fieldsAndValue = new ArrayList<>(); - + final List fieldsAndValues = new ArrayList<>(); + public FieldAndValueCollector() { super(); } - - public void add(FieldAndValue fieldAndValue) { - fieldsAndValue.add(fieldAndValue); + + public void add(FieldAndValueAndParameters fieldAndValueAndParameters) { + fieldsAndValues.add(fieldAndValueAndParameters); } - + public FieldAndValueCollector merge(FieldAndValueCollector other) { - fieldsAndValue.addAll(other.fieldsAndValue); + fieldsAndValues.addAll(other.fieldsAndValues); return this; } + public boolean isEmpty() { + return fieldsAndValues.isEmpty(); + } + public String columnsPhrase() { - return fieldsAndValue.stream() - .map(FieldAndValue::fieldName) + return fieldsAndValues.stream() + .map(FieldAndValueAndParameters::fieldName) .collect(Collectors.joining(", ", "(", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } public String valuesPhrase() { - return fieldsAndValue.stream() - .map(FieldAndValue::valuePhrase) + return fieldsAndValues.stream() + .map(FieldAndValueAndParameters::valuePhrase) .collect(Collectors.joining(", ", "values (", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } - + public String multiRowInsertValuesPhrase(int rowCount) { return IntStream.range(0, rowCount) .mapToObj(this::toSingleRowOfValues) .collect(Collectors.joining(", ", "values ", "")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } - + private String toSingleRowOfValues(int row) { - return fieldsAndValue.stream() - .map(fmv -> fmv.valuePhrase(row)) + return fieldsAndValues.stream() + .map(FieldAndValueAndParameters::valuePhrase) + .map(s -> String.format(s, row)) .collect(Collectors.joining(", ", "(", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } - - public static Collector collect() { + + public Map parameters() { + return fieldsAndValues.stream() + .map(FieldAndValueAndParameters::parameters) + .collect(HashMap::new, HashMap::putAll, HashMap::putAll); + } + + public static Collector collect() { return Collector.of(FieldAndValueCollector::new, FieldAndValueCollector::add, FieldAndValueCollector::merge); } -} \ No newline at end of file +} diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/GeneralInsertRenderer.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/GeneralInsertRenderer.java new file mode 100644 index 000000000..08eddb54f --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/GeneralInsertRenderer.java @@ -0,0 +1,78 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.insert.render; + +import java.util.Objects; +import java.util.Optional; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.insert.GeneralInsertModel; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.render.RenderingStrategy; +import org.mybatis.dynamic.sql.util.Validator; + +public class GeneralInsertRenderer { + + private final GeneralInsertModel model; + private final GeneralInsertValuePhraseVisitor visitor; + + private GeneralInsertRenderer(Builder builder) { + model = Objects.requireNonNull(builder.model); + RenderingContext renderingContext = RenderingContext + .withRenderingStrategy(Objects.requireNonNull(builder.renderingStrategy)) + .withStatementConfiguration(model.statementConfiguration()) + .build(); + visitor = new GeneralInsertValuePhraseVisitor(renderingContext); + } + + public GeneralInsertStatementProvider render() { + FieldAndValueCollector collector = model.columnMappings() + .map(m -> m.accept(visitor)) + .flatMap(Optional::stream) + .collect(FieldAndValueCollector.collect()); + + Validator.assertFalse(collector.isEmpty(), "ERROR.9"); //$NON-NLS-1$ + + String insertStatement = InsertRenderingUtilities.calculateInsertStatement(model.table(), collector); + + return DefaultGeneralInsertStatementProvider.withInsertStatement(insertStatement) + .withParameters(collector.parameters()) + .build(); + } + + public static Builder withInsertModel(GeneralInsertModel model) { + return new Builder().withInsertModel(model); + } + + public static class Builder { + private @Nullable GeneralInsertModel model; + private @Nullable RenderingStrategy renderingStrategy; + + public Builder withInsertModel(GeneralInsertModel model) { + this.model = model; + return this; + } + + public Builder withRenderingStrategy(RenderingStrategy renderingStrategy) { + this.renderingStrategy = renderingStrategy; + return this; + } + + public GeneralInsertRenderer build() { + return new GeneralInsertRenderer(this); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/GeneralInsertStatementProvider.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/GeneralInsertStatementProvider.java new file mode 100644 index 000000000..394a88ac2 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/GeneralInsertStatementProvider.java @@ -0,0 +1,24 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.insert.render; + +import java.util.Map; + +public interface GeneralInsertStatementProvider { + Map getParameters(); + + String getInsertStatement(); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/GeneralInsertValuePhraseVisitor.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/GeneralInsertValuePhraseVisitor.java new file mode 100644 index 000000000..914d710cf --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/GeneralInsertValuePhraseVisitor.java @@ -0,0 +1,96 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.insert.render; + +import java.util.Objects; +import java.util.Optional; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.render.RenderedParameterInfo; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.AbstractColumnMapping; +import org.mybatis.dynamic.sql.util.ConstantMapping; +import org.mybatis.dynamic.sql.util.GeneralInsertMappingVisitor; +import org.mybatis.dynamic.sql.util.NullMapping; +import org.mybatis.dynamic.sql.util.StringConstantMapping; +import org.mybatis.dynamic.sql.util.StringUtilities; +import org.mybatis.dynamic.sql.util.ValueMapping; +import org.mybatis.dynamic.sql.util.ValueOrNullMapping; +import org.mybatis.dynamic.sql.util.ValueWhenPresentMapping; + +public class GeneralInsertValuePhraseVisitor extends GeneralInsertMappingVisitor> { + + private final RenderingContext renderingContext; + + public GeneralInsertValuePhraseVisitor(RenderingContext renderingContext) { + this.renderingContext = Objects.requireNonNull(renderingContext); + } + + @Override + public Optional visit(NullMapping mapping) { + return buildNullFragment(mapping); + } + + @Override + public Optional visit(ConstantMapping mapping) { + return FieldAndValueAndParameters.withFieldName(mapping.columnName()) + .withValuePhrase(mapping.constant()) + .buildOptional(); + } + + @Override + public Optional visit(StringConstantMapping mapping) { + return FieldAndValueAndParameters.withFieldName(mapping.columnName()) + .withValuePhrase(StringUtilities.formatConstantForSQL(mapping.constant())) + .buildOptional(); + } + + @Override + public Optional visit(ValueMapping mapping) { + return buildValueFragment(mapping, mapping.value()); + } + + @Override + public Optional visit(ValueOrNullMapping mapping) { + return mapping.value().map(v -> buildValueFragment(mapping, v)) + .orElseGet(() -> buildNullFragment(mapping)); + } + + @Override + public Optional visit(ValueWhenPresentMapping mapping) { + return mapping.value().flatMap(v -> buildValueFragment(mapping, v)); + } + + private Optional buildValueFragment(AbstractColumnMapping mapping, + @Nullable Object value) { + return buildFragment(mapping, value); + } + + private Optional buildNullFragment(AbstractColumnMapping mapping) { + return FieldAndValueAndParameters.withFieldName(mapping.columnName()) + .withValuePhrase("null") //$NON-NLS-1$ + .buildOptional(); + } + + private Optional buildFragment(AbstractColumnMapping mapping, @Nullable Object value) { + RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo(mapping.column()); + + return FieldAndValueAndParameters.withFieldName(mapping.columnName()) + .withValuePhrase(parameterInfo.renderedPlaceHolder()) + .withParameter(parameterInfo.parameterMapKey(), value) + .buildOptional(); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/InsertRenderer.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/InsertRenderer.java index 99d34f3a4..9ff5858be 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/render/InsertRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/InsertRenderer.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,68 +15,57 @@ */ package org.mybatis.dynamic.sql.insert.render; -import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore; - import java.util.Objects; -import java.util.function.Function; +import java.util.Optional; +import org.jspecify.annotations.Nullable; import org.mybatis.dynamic.sql.insert.InsertModel; import org.mybatis.dynamic.sql.render.RenderingStrategy; -import org.mybatis.dynamic.sql.util.InsertMapping; +import org.mybatis.dynamic.sql.util.Validator; public class InsertRenderer { - private InsertModel model; - private RenderingStrategy renderingStrategy; - + private final InsertModel model; + private final ValuePhraseVisitor visitor; + private InsertRenderer(Builder builder) { model = Objects.requireNonNull(builder.model); - renderingStrategy = Objects.requireNonNull(builder.renderingStrategy); + visitor = new ValuePhraseVisitor(Objects.requireNonNull(builder.renderingStrategy)); } - + public InsertStatementProvider render() { - ValuePhraseVisitor visitor = new ValuePhraseVisitor(renderingStrategy); - FieldAndValueCollector collector = model.mapColumnMappings(toFieldAndValue(visitor)) + FieldAndValueCollector collector = model.columnMappings() + .map(m -> m.accept(visitor)) + .flatMap(Optional::stream) .collect(FieldAndValueCollector.collect()); - - return DefaultInsertStatementProvider.withRecord(model.record()) - .withInsertStatement(calculateInsertStatement(collector)) - .build(); - } - private String calculateInsertStatement(FieldAndValueCollector collector) { - return "insert into" //$NON-NLS-1$ - + spaceBefore(model.table().tableNameAtRuntime()) - + spaceBefore(collector.columnsPhrase()) - + spaceBefore(collector.valuesPhrase()); - } + Validator.assertFalse(collector.isEmpty(), "ERROR.10"); //$NON-NLS-1$ - private Function toFieldAndValue(ValuePhraseVisitor visitor) { - return insertMapping -> toFieldAndValue(visitor, insertMapping); - } - - private FieldAndValue toFieldAndValue(ValuePhraseVisitor visitor, InsertMapping insertMapping) { - return insertMapping.accept(visitor); + String insertStatement = InsertRenderingUtilities.calculateInsertStatement(model.table(), collector); + + return DefaultInsertStatementProvider.withRow(model.row()) + .withInsertStatement(insertStatement) + .build(); } - + public static Builder withInsertModel(InsertModel model) { return new Builder().withInsertModel(model); } - + public static class Builder { - private InsertModel model; - private RenderingStrategy renderingStrategy; - + private @Nullable InsertModel model; + private @Nullable RenderingStrategy renderingStrategy; + public Builder withInsertModel(InsertModel model) { this.model = model; return this; } - + public Builder withRenderingStrategy(RenderingStrategy renderingStrategy) { this.renderingStrategy = renderingStrategy; return this; } - + public InsertRenderer build() { return new InsertRenderer<>(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/InsertRenderingUtilities.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/InsertRenderingUtilities.java new file mode 100644 index 000000000..a65b56b85 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/InsertRenderingUtilities.java @@ -0,0 +1,45 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.insert.render; + +import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore; + +import org.mybatis.dynamic.sql.SqlColumn; +import org.mybatis.dynamic.sql.SqlTable; +import org.mybatis.dynamic.sql.exception.InvalidSqlException; +import org.mybatis.dynamic.sql.util.Messages; + +public class InsertRenderingUtilities { + private InsertRenderingUtilities() {} + + public static String calculateInsertStatement(SqlTable table, FieldAndValueCollector collector) { + String statementStart = calculateInsertStatementStart(table); + String columnsPhrase = collector.columnsPhrase(); + String valuesPhrase = collector.valuesPhrase(); + + return statementStart + spaceBefore(columnsPhrase) + spaceBefore(valuesPhrase); + } + + public static String calculateInsertStatementStart(SqlTable table) { + return "insert into " + table.tableName(); //$NON-NLS-1$ + } + + public static String getMappedPropertyName(SqlColumn column) { + return column.javaProperty().orElseThrow(() -> + new InvalidSqlException(Messages + .getString("ERROR.50", column.name()))); //$NON-NLS-1$ + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/InsertSelectRenderer.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/InsertSelectRenderer.java index 9c6c07cd8..7740309bd 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/render/InsertSelectRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/InsertSelectRenderer.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,71 +15,76 @@ */ package org.mybatis.dynamic.sql.insert.render; -import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore; +import static org.mybatis.dynamic.sql.util.StringUtilities.spaceAfter; import java.util.Objects; -import java.util.Optional; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; import org.mybatis.dynamic.sql.SqlColumn; import org.mybatis.dynamic.sql.insert.InsertColumnListModel; import org.mybatis.dynamic.sql.insert.InsertSelectModel; +import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.render.RenderingStrategy; -import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; +import org.mybatis.dynamic.sql.select.render.SubQueryRenderer; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; public class InsertSelectRenderer { - private InsertSelectModel model; - private RenderingStrategy renderingStrategy; - + private final InsertSelectModel model; + private final RenderingContext renderingContext; + private InsertSelectRenderer(Builder builder) { model = Objects.requireNonNull(builder.model); - renderingStrategy = Objects.requireNonNull(builder.renderingStrategy); + renderingContext = RenderingContext.withRenderingStrategy(Objects.requireNonNull(builder.renderingStrategy)) + .withStatementConfiguration(model.statementConfiguration()) + .build(); } - + public InsertSelectStatementProvider render() { - SelectStatementProvider selectStatement = model.selectModel().render(renderingStrategy); - - return DefaultInsertSelectStatementProvider.withInsertStatement(calculateInsertStatement(selectStatement)) - .withParameters(selectStatement.getParameters()) + String statementStart = InsertRenderingUtilities.calculateInsertStatementStart(model.table()); + String columnsPhrase = calculateColumnsPhrase(); + String prefix = statementStart + spaceAfter(columnsPhrase); + + FragmentAndParameters fragmentAndParameters = SubQueryRenderer.withSelectModel(model.selectModel()) + .withRenderingContext(renderingContext) + .withPrefix(prefix) + .build() + .render(); + + return DefaultGeneralInsertStatementProvider.withInsertStatement(fragmentAndParameters.fragment()) + .withParameters(fragmentAndParameters.parameters()) .build(); } - - private String calculateInsertStatement(SelectStatementProvider selectStatement) { - return "insert into" //$NON-NLS-1$ - + spaceBefore(model.table().tableNameAtRuntime()) - + spaceBefore(calculateColumnsPhrase()) - + spaceBefore(selectStatement.getSelectStatement()); - } - - private Optional calculateColumnsPhrase() { - return model.columnList() - .map(this::calculateColumnsPhrase); + + private String calculateColumnsPhrase() { + return model.columnList().map(this::calculateColumnsPhrase).orElse(""); //$NON-NLS-1$ } - + private String calculateColumnsPhrase(InsertColumnListModel columnList) { - return columnList.mapColumns(SqlColumn::name) - .collect(Collectors.joining(", ", "(", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + return columnList.columns() + .map(SqlColumn::name) + .collect(Collectors.joining(", ", " (", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } - + public static Builder withInsertSelectModel(InsertSelectModel model) { return new Builder().withInsertSelectModel(model); } - + public static class Builder { - private InsertSelectModel model; - private RenderingStrategy renderingStrategy; - + private @Nullable InsertSelectModel model; + private @Nullable RenderingStrategy renderingStrategy; + public Builder withInsertSelectModel(InsertSelectModel model) { this.model = model; return this; } - + public Builder withRenderingStrategy(RenderingStrategy renderingStrategy) { this.renderingStrategy = renderingStrategy; return this; } - + public InsertSelectRenderer build() { return new InsertSelectRenderer(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/InsertSelectStatementProvider.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/InsertSelectStatementProvider.java index 8ec8961e8..5545da79a 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/render/InsertSelectStatementProvider.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/InsertSelectStatementProvider.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/InsertStatementProvider.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/InsertStatementProvider.java index b74a7e52f..bd40a0e1f 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/render/InsertStatementProvider.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/InsertStatementProvider.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -16,7 +16,17 @@ package org.mybatis.dynamic.sql.insert.render; public interface InsertStatementProvider { - T getRecord(); - + /** + * Return the row associated with this insert statement. + * + * @return the row associated with this insert statement. + */ + T getRow(); + + /** + * Return the formatted insert statement. + * + * @return the formatted insert statement. + */ String getInsertStatement(); } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/MultiRowInsertRenderer.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/MultiRowInsertRenderer.java index 8d3b88a87..5f3146a77 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/render/MultiRowInsertRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/MultiRowInsertRenderer.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -19,54 +19,60 @@ import java.util.Objects; +import org.jspecify.annotations.Nullable; import org.mybatis.dynamic.sql.insert.MultiRowInsertModel; import org.mybatis.dynamic.sql.render.RenderingStrategy; public class MultiRowInsertRenderer { - private MultiRowInsertModel model; - private RenderingStrategy renderingStrategy; - + private final MultiRowInsertModel model; + private final MultiRowValuePhraseVisitor visitor; + private MultiRowInsertRenderer(Builder builder) { model = Objects.requireNonNull(builder.model); - renderingStrategy = Objects.requireNonNull(builder.renderingStrategy); + // the prefix is a generic format that will be resolved below with String.format(...) + visitor = new MultiRowValuePhraseVisitor(Objects.requireNonNull(builder.renderingStrategy), + "records[%s]"); //$NON-NLS-1$ } - + public MultiRowInsertStatementProvider render() { - ValuePhraseVisitor visitor = new MultiRowValuePhraseVisitor(renderingStrategy); - FieldAndValueCollector collector = model.mapColumnMappings(MultiRowRenderingUtilities.toFieldAndValue(visitor)) + FieldAndValueCollector collector = model.columnMappings() + .map(m -> m.accept(visitor)) .collect(FieldAndValueCollector.collect()); - + + String insertStatement = calculateInsertStatement(collector); + return new DefaultMultiRowInsertStatementProvider.Builder().withRecords(model.records()) - .withInsertStatement(calculateInsertStatement(collector)) + .withInsertStatement(insertStatement) .build(); } - + private String calculateInsertStatement(FieldAndValueCollector collector) { - return "insert into" //$NON-NLS-1$ - + spaceBefore(model.table().tableNameAtRuntime()) - + spaceBefore(collector.columnsPhrase()) - + spaceBefore(collector.multiRowInsertValuesPhrase(model.recordCount())); + String statementStart = InsertRenderingUtilities.calculateInsertStatementStart(model.table()); + String columnsPhrase = collector.columnsPhrase(); + String valuesPhrase = collector.multiRowInsertValuesPhrase(model.recordCount()); + + return statementStart + spaceBefore(columnsPhrase) + spaceBefore(valuesPhrase); } - + public static Builder withMultiRowInsertModel(MultiRowInsertModel model) { return new Builder().withMultiRowInsertModel(model); } - + public static class Builder { - private MultiRowInsertModel model; - private RenderingStrategy renderingStrategy; - + private @Nullable MultiRowInsertModel model; + private @Nullable RenderingStrategy renderingStrategy; + public Builder withMultiRowInsertModel(MultiRowInsertModel model) { this.model = model; return this; } - + public Builder withRenderingStrategy(RenderingStrategy renderingStrategy) { this.renderingStrategy = renderingStrategy; return this; } - + public MultiRowInsertRenderer build() { return new MultiRowInsertRenderer<>(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/MultiRowInsertStatementProvider.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/MultiRowInsertStatementProvider.java index 7987f205f..de5d87797 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/render/MultiRowInsertStatementProvider.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/MultiRowInsertStatementProvider.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -18,8 +18,8 @@ import java.util.List; public interface MultiRowInsertStatementProvider { - + String getInsertStatement(); - + List getRecords(); } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/MultiRowRenderingUtilities.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/MultiRowRenderingUtilities.java deleted file mode 100644 index 489b48d4d..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/insert/render/MultiRowRenderingUtilities.java +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright 2016-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.insert.render; - -import java.util.function.Function; - -import org.mybatis.dynamic.sql.util.InsertMapping; - -public class MultiRowRenderingUtilities { - - private MultiRowRenderingUtilities() {} - - public static Function toFieldAndValue(ValuePhraseVisitor visitor) { - return insertMapping -> MultiRowRenderingUtilities.toFieldAndValue(visitor, insertMapping); - } - - public static FieldAndValue toFieldAndValue(ValuePhraseVisitor visitor, InsertMapping insertMapping) { - return insertMapping.accept(visitor); - } -} diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/MultiRowValuePhraseVisitor.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/MultiRowValuePhraseVisitor.java index 15080828e..216fdfcbf 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/render/MultiRowValuePhraseVisitor.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/MultiRowValuePhraseVisitor.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,27 +15,77 @@ */ package org.mybatis.dynamic.sql.insert.render; -import java.util.function.Function; - import org.mybatis.dynamic.sql.SqlColumn; import org.mybatis.dynamic.sql.render.RenderingStrategy; +import org.mybatis.dynamic.sql.util.ConstantMapping; +import org.mybatis.dynamic.sql.util.MappedColumnMapping; +import org.mybatis.dynamic.sql.util.MultiRowInsertMappingVisitor; +import org.mybatis.dynamic.sql.util.NullMapping; import org.mybatis.dynamic.sql.util.PropertyMapping; +import org.mybatis.dynamic.sql.util.RowMapping; +import org.mybatis.dynamic.sql.util.StringConstantMapping; +import org.mybatis.dynamic.sql.util.StringUtilities; + +public class MultiRowValuePhraseVisitor extends MultiRowInsertMappingVisitor { + protected final RenderingStrategy renderingStrategy; + protected final String prefix; + + protected MultiRowValuePhraseVisitor(RenderingStrategy renderingStrategy, String prefix) { + this.renderingStrategy = renderingStrategy; + this.prefix = prefix; + } -public class MultiRowValuePhraseVisitor extends ValuePhraseVisitor { + @Override + public FieldAndValueAndParameters visit(NullMapping mapping) { + return FieldAndValueAndParameters.withFieldName(mapping.columnName()) + .withValuePhrase("null") //$NON-NLS-1$ + .build(); + } - public MultiRowValuePhraseVisitor(RenderingStrategy renderingStrategy) { - super(renderingStrategy); + @Override + public FieldAndValueAndParameters visit(ConstantMapping mapping) { + return FieldAndValueAndParameters.withFieldName(mapping.columnName()) + .withValuePhrase(mapping.constant()) + .build(); } @Override - public FieldAndValue visit(PropertyMapping mapping) { - return FieldAndValue.withFieldName(mapping.mapColumn(SqlColumn::name)) - .withValuePhrase(mapping.mapColumn(toMultiRowJdbcPlaceholder(mapping.property()))) + public FieldAndValueAndParameters visit(StringConstantMapping mapping) { + return FieldAndValueAndParameters.withFieldName(mapping.columnName()) + .withValuePhrase(StringUtilities.formatConstantForSQL(mapping.constant())) .build(); } - private Function, String> toMultiRowJdbcPlaceholder(String parameterName) { - return column -> renderingStrategy.getFormattedJdbcPlaceholder(column, "records[%s]", //$NON-NLS-1$ - parameterName); + @Override + public FieldAndValueAndParameters visit(PropertyMapping mapping) { + return FieldAndValueAndParameters.withFieldName(mapping.columnName()) + .withValuePhrase(calculateJdbcPlaceholder(mapping.column(), mapping.property())) + .build(); + } + + @Override + public FieldAndValueAndParameters visit(RowMapping mapping) { + return FieldAndValueAndParameters.withFieldName(mapping.columnName()) + .withValuePhrase(calculateJdbcPlaceholder(mapping.column())) + .build(); + } + + @Override + public FieldAndValueAndParameters visit(MappedColumnMapping mapping) { + return FieldAndValueAndParameters.withFieldName(mapping.columnName()) + .withValuePhrase(calculateJdbcPlaceholder( + mapping.column(), + InsertRenderingUtilities.getMappedPropertyName(mapping.column())) + ) + .build(); + } + + private String calculateJdbcPlaceholder(SqlColumn column) { + return column.renderingStrategy().orElse(renderingStrategy).getRecordBasedInsertBinding(column, prefix); + } + + private String calculateJdbcPlaceholder(SqlColumn column, String parameterName) { + return column.renderingStrategy().orElse(renderingStrategy) + .getRecordBasedInsertBinding(column, prefix, parameterName); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/ValuePhraseVisitor.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/ValuePhraseVisitor.java index 0e7235178..d628c77ef 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/render/ValuePhraseVisitor.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/ValuePhraseVisitor.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,53 +15,99 @@ */ package org.mybatis.dynamic.sql.insert.render; -import java.util.function.Function; +import java.util.Optional; import org.mybatis.dynamic.sql.SqlColumn; import org.mybatis.dynamic.sql.render.RenderingStrategy; import org.mybatis.dynamic.sql.util.ConstantMapping; import org.mybatis.dynamic.sql.util.InsertMappingVisitor; +import org.mybatis.dynamic.sql.util.MappedColumnMapping; +import org.mybatis.dynamic.sql.util.MappedColumnWhenPresentMapping; import org.mybatis.dynamic.sql.util.NullMapping; import org.mybatis.dynamic.sql.util.PropertyMapping; +import org.mybatis.dynamic.sql.util.PropertyWhenPresentMapping; +import org.mybatis.dynamic.sql.util.RowMapping; import org.mybatis.dynamic.sql.util.StringConstantMapping; +import org.mybatis.dynamic.sql.util.StringUtilities; + +public class ValuePhraseVisitor extends InsertMappingVisitor> { + + protected final RenderingStrategy renderingStrategy; -public class ValuePhraseVisitor implements InsertMappingVisitor { - - protected RenderingStrategy renderingStrategy; - public ValuePhraseVisitor(RenderingStrategy renderingStrategy) { this.renderingStrategy = renderingStrategy; } @Override - public FieldAndValue visit(NullMapping mapping) { - return FieldAndValue.withFieldName(mapping.mapColumn(SqlColumn::name)) + public Optional visit(NullMapping mapping) { + return FieldAndValueAndParameters.withFieldName(mapping.columnName()) .withValuePhrase("null") //$NON-NLS-1$ - .build(); + .buildOptional(); } @Override - public FieldAndValue visit(ConstantMapping mapping) { - return FieldAndValue.withFieldName(mapping.mapColumn(SqlColumn::name)) + public Optional visit(ConstantMapping mapping) { + return FieldAndValueAndParameters.withFieldName(mapping.columnName()) .withValuePhrase(mapping.constant()) - .build(); + .buildOptional(); + } + + @Override + public Optional visit(StringConstantMapping mapping) { + return FieldAndValueAndParameters.withFieldName(mapping.columnName()) + .withValuePhrase(StringUtilities.formatConstantForSQL(mapping.constant())) + .buildOptional(); + } + + @Override + public Optional visit(PropertyMapping mapping) { + return FieldAndValueAndParameters.withFieldName(mapping.columnName()) + .withValuePhrase(calculateJdbcPlaceholder(mapping.column(), mapping.property())) + .buildOptional(); + } + + @Override + public Optional visit(PropertyWhenPresentMapping mapping) { + if (mapping.shouldRender()) { + return visit((PropertyMapping) mapping); + } else { + return Optional.empty(); + } } @Override - public FieldAndValue visit(StringConstantMapping mapping) { - return FieldAndValue.withFieldName(mapping.mapColumn(SqlColumn::name)) - .withValuePhrase("'" + mapping.constant() + "'") //$NON-NLS-1$ //$NON-NLS-2$ - .build(); + public Optional visit(RowMapping mapping) { + return FieldAndValueAndParameters.withFieldName(mapping.columnName()) + .withValuePhrase(calculateJdbcPlaceholder(mapping.column())) + .buildOptional(); } - + @Override - public FieldAndValue visit(PropertyMapping mapping) { - return FieldAndValue.withFieldName(mapping.mapColumn(SqlColumn::name)) - .withValuePhrase(mapping.mapColumn(toJdbcPlaceholder(mapping.property()))) - .build(); + public Optional visit(MappedColumnMapping mapping) { + return FieldAndValueAndParameters.withFieldName(mapping.columnName()) + .withValuePhrase(calculateJdbcPlaceholder( + mapping.column(), + InsertRenderingUtilities.getMappedPropertyName(mapping.column())) + ) + .buildOptional(); } - - private Function, String> toJdbcPlaceholder(String parameterName) { - return column -> renderingStrategy.getFormattedJdbcPlaceholder(column, "record", parameterName); //$NON-NLS-1$ + + @Override + public Optional visit(MappedColumnWhenPresentMapping mapping) { + if (mapping.shouldRender()) { + return visit((MappedColumnMapping) mapping); + } else { + return Optional.empty(); + } + } + + private String calculateJdbcPlaceholder(SqlColumn column) { + return column.renderingStrategy().orElse(renderingStrategy) + .getRecordBasedInsertBinding(column, "row"); //$NON-NLS-1$ + } + + private String calculateJdbcPlaceholder(SqlColumn column, String parameterName) { + return column.renderingStrategy().orElse(renderingStrategy) + .getRecordBasedInsertBinding(column, "row", parameterName); //$NON-NLS-1$ } } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/package-info.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/package-info.java new file mode 100644 index 000000000..02cfd6efa --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +@NullMarked +package org.mybatis.dynamic.sql.insert.render; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/mybatis/dynamic/sql/package-info.java b/src/main/java/org/mybatis/dynamic/sql/package-info.java new file mode 100644 index 000000000..7555e2e26 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +@NullMarked +package org.mybatis.dynamic.sql; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/mybatis/dynamic/sql/render/ExplicitTableAliasCalculator.java b/src/main/java/org/mybatis/dynamic/sql/render/ExplicitTableAliasCalculator.java new file mode 100644 index 000000000..1cb388f85 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/render/ExplicitTableAliasCalculator.java @@ -0,0 +1,60 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.render; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import org.mybatis.dynamic.sql.SqlTable; + +public class ExplicitTableAliasCalculator implements TableAliasCalculator { + private final Map aliases; + + protected ExplicitTableAliasCalculator(Map aliases) { + this.aliases = Objects.requireNonNull(aliases); + } + + @Override + public Optional aliasForColumn(SqlTable table) { + return explicitAliasOrTableAlias(table); + } + + @Override + public Optional aliasForTable(SqlTable table) { + return explicitAliasOrTableAlias(table); + } + + private Optional explicitAliasOrTableAlias(SqlTable table) { + String alias = aliases.get(table); + if (alias == null) { + return table.tableAlias(); + } else { + return Optional.of(alias); + } + } + + public static TableAliasCalculator of(SqlTable table, String alias) { + Map tableAliases = new HashMap<>(); + tableAliases.put(table, alias); + return of(tableAliases); + } + + public static TableAliasCalculator of(Map aliases) { + return new ExplicitTableAliasCalculator(aliases); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/render/GuaranteedTableAliasCalculator.java b/src/main/java/org/mybatis/dynamic/sql/render/GuaranteedTableAliasCalculator.java index 8862a5a18..80e4bfbb3 100644 --- a/src/main/java/org/mybatis/dynamic/sql/render/GuaranteedTableAliasCalculator.java +++ b/src/main/java/org/mybatis/dynamic/sql/render/GuaranteedTableAliasCalculator.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -23,11 +23,10 @@ /** * Returns the alias for a table if specified, or the table name itself. * This is useful for join rendering when we always want to have an alias for the table. - * + * * @author Jeff Butler - * */ -public class GuaranteedTableAliasCalculator extends TableAliasCalculator { +public class GuaranteedTableAliasCalculator extends ExplicitTableAliasCalculator { private GuaranteedTableAliasCalculator(Map aliases) { super(aliases); @@ -35,11 +34,14 @@ private GuaranteedTableAliasCalculator(Map aliases) { @Override public Optional aliasForColumn(SqlTable table) { - return super.aliasForColumn(table) - .map(Optional::of) - .orElseGet(() -> Optional.of(table.tableNameAtRuntime())); + Optional alias = super.aliasForColumn(table); + if (alias.isPresent()) { + return alias; + } else { + return Optional.of(table.tableName()); + } } - + public static TableAliasCalculator of(Map aliases) { return new GuaranteedTableAliasCalculator(aliases); } diff --git a/src/main/java/org/mybatis/dynamic/sql/render/MyBatis3RenderingStrategy.java b/src/main/java/org/mybatis/dynamic/sql/render/MyBatis3RenderingStrategy.java index 423bf56c1..1d5563c2a 100644 --- a/src/main/java/org/mybatis/dynamic/sql/render/MyBatis3RenderingStrategy.java +++ b/src/main/java/org/mybatis/dynamic/sql/render/MyBatis3RenderingStrategy.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -34,19 +34,36 @@ public String getFormattedJdbcPlaceholder(BindableColumn column, String prefi + "." //$NON-NLS-1$ + parameterName + renderJdbcType(column) + + renderJavaType(column) + renderTypeHandler(column) + "}"; //$NON-NLS-1$ } - + + @Override + public String getRecordBasedInsertBinding(BindableColumn column, String parameterName) { + return "#{" //$NON-NLS-1$ + + parameterName + + renderJdbcType(column) + + renderJavaType(column) + + renderTypeHandler(column) + + "}"; //$NON-NLS-1$ + } + private String renderTypeHandler(BindableColumn column) { return column.typeHandler() .map(th -> ",typeHandler=" + th) //$NON-NLS-1$ .orElse(""); //$NON-NLS-1$ } - + private String renderJdbcType(BindableColumn column) { return column.jdbcType() .map(jt -> ",jdbcType=" + jt.getName()) //$NON-NLS-1$ .orElse(""); //$NON-NLS-1$ } + + private String renderJavaType(BindableColumn column) { + return column.javaType() + .map(jt -> ",javaType=" + jt.getName()) //$NON-NLS-1$ + .orElse(""); //$NON-NLS-1$ + } } diff --git a/src/main/java/org/mybatis/dynamic/sql/render/RenderedParameterInfo.java b/src/main/java/org/mybatis/dynamic/sql/render/RenderedParameterInfo.java new file mode 100644 index 000000000..5c8187ef2 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/render/RenderedParameterInfo.java @@ -0,0 +1,25 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.render; + +import java.util.Objects; + +public record RenderedParameterInfo(String parameterMapKey, String renderedPlaceHolder) { + public RenderedParameterInfo(String parameterMapKey, String renderedPlaceHolder) { + this.parameterMapKey = Objects.requireNonNull(parameterMapKey); + this.renderedPlaceHolder = Objects.requireNonNull(renderedPlaceHolder); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/render/RenderingContext.java b/src/main/java/org/mybatis/dynamic/sql/render/RenderingContext.java new file mode 100644 index 000000000..4e1457067 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/render/RenderingContext.java @@ -0,0 +1,173 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.render; + +import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.SqlColumn; +import org.mybatis.dynamic.sql.SqlTable; +import org.mybatis.dynamic.sql.configuration.StatementConfiguration; + +/** + * This class encapsulates all the supporting items related to rendering, and contains many utility methods + * used during the rendering process. + * + * @since 1.5.1 + * @author Jeff Butler + */ +public class RenderingContext { + + private final RenderingStrategy renderingStrategy; + private final AtomicInteger sequence; + private final TableAliasCalculator tableAliasCalculator; + private final @Nullable String configuredParameterName; + private final String calculatedParameterName; + private final StatementConfiguration statementConfiguration; + + private RenderingContext(Builder builder) { + renderingStrategy = Objects.requireNonNull(builder.renderingStrategy); + configuredParameterName = builder.parameterName; + tableAliasCalculator = Objects.requireNonNull(builder.tableAliasCalculator); + statementConfiguration = Objects.requireNonNull(builder.statementConfiguration); + + // reasonable defaults + sequence = builder.sequence == null ? new AtomicInteger(1) : builder.sequence; + calculatedParameterName = builder.parameterName == null ? RenderingStrategy.DEFAULT_PARAMETER_PREFIX + : builder.parameterName + "." + RenderingStrategy.DEFAULT_PARAMETER_PREFIX; //$NON-NLS-1$ + } + + private String nextMapKey() { + return renderingStrategy.formatParameterMapKey(sequence); + } + + private String renderedPlaceHolder(String mapKey, BindableColumn column) { + return column.renderingStrategy().orElse(renderingStrategy) + .getFormattedJdbcPlaceholder(column, calculatedParameterName, mapKey); + } + + public RenderedParameterInfo calculateFetchFirstRowsParameterInfo() { + String mapKey = renderingStrategy.formatParameterMapKeyForFetchFirstRows(sequence); + return new RenderedParameterInfo(mapKey, + renderingStrategy.getFormattedJdbcPlaceholderForPagingParameters(calculatedParameterName, mapKey)); + } + + public RenderedParameterInfo calculateLimitParameterInfo() { + String mapKey = renderingStrategy.formatParameterMapKeyForLimit(sequence); + return new RenderedParameterInfo(mapKey, + renderingStrategy.getFormattedJdbcPlaceholderForPagingParameters(calculatedParameterName, mapKey)); + } + + public RenderedParameterInfo calculateOffsetParameterInfo() { + String mapKey = renderingStrategy.formatParameterMapKeyForOffset(sequence); + return new RenderedParameterInfo(mapKey, + renderingStrategy.getFormattedJdbcPlaceholderForPagingParameters(calculatedParameterName, mapKey)); + } + + public RenderedParameterInfo calculateParameterInfo(BindableColumn column) { + String mapKey = nextMapKey(); + return new RenderedParameterInfo(mapKey, renderedPlaceHolder(mapKey, column)); + } + + public String aliasedColumnName(SqlColumn column) { + return tableAliasCalculator.aliasForColumn(column.table()) + .map(alias -> aliasedColumnName(column, alias)) + .orElseGet(column::name); + } + + public String aliasedColumnName(SqlColumn column, String explicitAlias) { + return explicitAlias + "." + column.name(); //$NON-NLS-1$ + } + + public String aliasedTableName(SqlTable table) { + return tableAliasCalculator.aliasForTable(table) + .map(a -> table.tableName() + spaceBefore(a)) + .orElseGet(table::tableName); + } + + public boolean isNonRenderingClauseAllowed() { + return statementConfiguration.isNonRenderingWhereClauseAllowed(); + } + + /** + * Create a new rendering context based on this, with the table alias calculator modified to include the + * specified child table alias calculator. This is used by the query expression renderer when the alias calculator + * may change during rendering. + * + * @param childTableAliasCalculator the child table alias calculator + * @return a new rendering context whose table alias calculator is composed of the former calculator as parent, and + * the new child calculator + */ + public RenderingContext withChildTableAliasCalculator(TableAliasCalculator childTableAliasCalculator) { + TableAliasCalculator tac = new TableAliasCalculatorWithParent.Builder() + .withParent(tableAliasCalculator) + .withChild(childTableAliasCalculator) + .build(); + + return new Builder() + .withRenderingStrategy(this.renderingStrategy) + .withSequence(this.sequence) + .withParameterName(this.configuredParameterName) + .withTableAliasCalculator(tac) + .withStatementConfiguration(statementConfiguration) + .build(); + } + + public static Builder withRenderingStrategy(RenderingStrategy renderingStrategy) { + return new Builder().withRenderingStrategy(renderingStrategy); + } + + public static class Builder { + private @Nullable RenderingStrategy renderingStrategy; + private @Nullable AtomicInteger sequence; + private @Nullable TableAliasCalculator tableAliasCalculator = TableAliasCalculator.empty(); + private @Nullable String parameterName; + private @Nullable StatementConfiguration statementConfiguration; + + public Builder withRenderingStrategy(RenderingStrategy renderingStrategy) { + this.renderingStrategy = renderingStrategy; + return this; + } + + public Builder withSequence(AtomicInteger sequence) { + this.sequence = sequence; + return this; + } + + public Builder withTableAliasCalculator(TableAliasCalculator tableAliasCalculator) { + this.tableAliasCalculator = tableAliasCalculator; + return this; + } + + public Builder withParameterName(@Nullable String parameterName) { + this.parameterName = parameterName; + return this; + } + + public Builder withStatementConfiguration(StatementConfiguration statementConfiguration) { + this.statementConfiguration = statementConfiguration; + return this; + } + + public RenderingContext build() { + return new RenderingContext(this); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/render/RenderingStrategies.java b/src/main/java/org/mybatis/dynamic/sql/render/RenderingStrategies.java index b6651d784..d3c17af78 100644 --- a/src/main/java/org/mybatis/dynamic/sql/render/RenderingStrategies.java +++ b/src/main/java/org/mybatis/dynamic/sql/render/RenderingStrategies.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -17,8 +17,8 @@ public class RenderingStrategies { private RenderingStrategies() {} - + public static final RenderingStrategy MYBATIS3 = new MyBatis3RenderingStrategy(); - + public static final RenderingStrategy SPRING_NAMED_PARAMETER = new SpringNamedParameterRenderingStrategy(); } diff --git a/src/main/java/org/mybatis/dynamic/sql/render/RenderingStrategy.java b/src/main/java/org/mybatis/dynamic/sql/render/RenderingStrategy.java index 9b76a4413..adf66115c 100644 --- a/src/main/java/org/mybatis/dynamic/sql/render/RenderingStrategy.java +++ b/src/main/java/org/mybatis/dynamic/sql/render/RenderingStrategy.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -19,32 +19,164 @@ import org.mybatis.dynamic.sql.BindableColumn; +/** + * A rendering strategy is used to generate a platform specific binding. + * + *

Rendering strategies are used during the rendering phase of statement generation. + * All generated SQL statements include the generated statement itself, and a map of parameters that + * should be bound to the statement at execution time. For example, a generated select statement may + * look like this when rendered for MyBatis: + * + *

select foo from bar where id = #{parameters.p1,jdbcType=INTEGER} + * + *

In this case, the binding is #{parameters.p1,jdbcType=INTEGER}. MyBatis knows how to interpret this + * binding - it will look for a value in the parameters.p1 property of the parameter object + * passed to the statement and bind it as a prepared statement parameter when executing the statement. + */ public abstract class RenderingStrategy { + public static final String DEFAULT_PARAMETER_PREFIX = "parameters"; //$NON-NLS-1$ + /** - * Rendering strategy for MyBatis3. - * - * @deprecated use {@link RenderingStrategies#MYBATIS3} instead + * Generate a unique key that can be used to place a parameter value in the parameter map. + * + * @param sequence a sequence for calculating a unique value + * @return a key used to place the parameter value in the parameter map */ - @Deprecated - @SuppressWarnings("squid:S2390") - public static final RenderingStrategy MYBATIS3 = new MyBatis3RenderingStrategy(); + public String formatParameterMapKey(AtomicInteger sequence) { + return "p" + sequence.getAndIncrement(); //$NON-NLS-1$ + } /** - * Rendering strategy for Spring JDBC Template Named Parameters. - * - * @deprecated use {@link RenderingStrategies#SPRING_NAMED_PARAMETER} instead + * Return a parameter map key intended as a parameter for a fetch first query. + * + *

By default, this parameter is treated the same as any other. This method is a hook to support + * MyBatis Spring Batch. + * + * @param sequence a sequence for calculating a unique value + * @return a key used to place the parameter value in the parameter map */ - @Deprecated - @SuppressWarnings("squid:S2390") - public static final RenderingStrategy SPRING_NAMED_PARAMETER = new SpringNamedParameterRenderingStrategy(); + public String formatParameterMapKeyForFetchFirstRows(AtomicInteger sequence) { + return formatParameterMapKey(sequence); + } - public static final String DEFAULT_PARAMETER_PREFIX = "parameters"; //$NON-NLS-1$ - - public static String formatParameterMapKey(AtomicInteger sequence) { - return "p" + sequence.getAndIncrement(); //$NON-NLS-1$ + /** + * Return a parameter map key intended as a parameter for a limit query. + * + *

By default, this parameter is treated the same as any other. This method is a hook to support + * MyBatis Spring Batch. + * + * @param sequence a sequence for calculating a unique value + * @return a key used to place the parameter value in the parameter map + */ + public String formatParameterMapKeyForLimit(AtomicInteger sequence) { + return formatParameterMapKey(sequence); } - + + /** + * Return a parameter map key intended as a parameter for a query offset. + * + *

By default, this parameter is treated the same as any other. This method is a hook to support + * MyBatis Spring Batch. + * + * @param sequence a sequence for calculating a unique value + * @return a key used to place the parameter value in the parameter map + */ + public String formatParameterMapKeyForOffset(AtomicInteger sequence) { + return formatParameterMapKey(sequence); + } + + /** + * This method generates a binding for a parameter to a placeholder in a generated SQL statement. + * + *

This binding is appropriate when there can be a mapping between a parameter and a known target column, + * In MyBatis, the binding can specify type information based on the column. The bindings are specific + * to the target framework. + * + *

For MyBatis, a binding looks like this: "#{prefix.parameterName,jdbcType=xxx,typeHandler=xxx,javaType=xxx}" + * + *

For Spring, a binding looks like this: ":parameterName" + * + * @param column column definition used for generating type details in a MyBatis binding. Ignored for Spring. + * @param prefix parameter prefix used for locating the parameters in a SQL provider object. Typically, will be + * {@link RenderingStrategy#DEFAULT_PARAMETER_PREFIX}. This is ignored for Spring. + * @param parameterName name of the parameter. Typically generated by calling + * {@link RenderingStrategy#formatParameterMapKey(AtomicInteger)} + * @return the generated binding + */ public abstract String getFormattedJdbcPlaceholder(BindableColumn column, String prefix, String parameterName); + /** + * This method generates a binding for a parameter to a placeholder in a generated SQL statement. + * + *

This binding is appropriate when the parameter is bound to placeholder that is not a known column (such as + * a limit or offset parameter). The bindings are specific to the target framework. + * + *

For MyBatis, a binding looks like this: "#{prefix.parameterName}" + * + *

For Spring, a binding looks like this: ":parameterName" + * + * @param prefix parameter prefix used for locating the parameters in a SQL provider object. Typically, will be + * {@link RenderingStrategy#DEFAULT_PARAMETER_PREFIX}. This is ignored for Spring. + * @param parameterName name of the parameter. Typically generated by calling + * {@link RenderingStrategy#formatParameterMapKey(AtomicInteger)} + * @return the generated binding + */ public abstract String getFormattedJdbcPlaceholder(String prefix, String parameterName); + + /** + * This method generates a binding for a parameter to a placeholder in a generated SQL statement. + * + *

This method is used to generate bindings for limit, offset, and fetch first parameters. By default, these + * parameters are treated the same as any other. This method supports MyBatis Spring Batch integration where the + * parameter keys have predefined values and need special handling. + * + * @param prefix parameter prefix used for locating the parameters in a SQL provider object. Typically, will be + * {@link RenderingStrategy#DEFAULT_PARAMETER_PREFIX}. This is ignored for Spring. + * @param parameterName name of the parameter. Typically generated by calling + * {@link RenderingStrategy#formatParameterMapKey(AtomicInteger)} + * @return the generated binding + */ + public String getFormattedJdbcPlaceholderForPagingParameters(String prefix, String parameterName) { + return getFormattedJdbcPlaceholder(prefix, parameterName); + } + + /** + * This method generates a binding for a parameter to a placeholder in a row based insert statement. + * + *

This binding is specifically for use with insert, batch insert, and multirow insert statements. + * These statements bind parameters to properties of a row class. The Spring implementation changes the binding + * to match values expected for a these insert statements. For MyBatis, the binding is the same + * as {@link RenderingStrategy#getFormattedJdbcPlaceholder(BindableColumn, String, String)}. + * + *

For MyBatis, a binding looks like this: "#{prefix.parameterName,jdbcType=xxx,typeHandler=xxx,javaType=xxx}" + * + *

For Spring, a binding looks like this: ":prefix.parameterName" + * + * @param column column definition used for generating type details in a MyBatis binding. Ignored for Spring. + * @param prefix parameter prefix used for locating the parameters in a SQL provider object. Typically, will be + * either "row" or "records[x]" to match the properties of the generated statement object class. + * @param parameterName name of the parameter. Typically, this is a property in the row class associated with the + * insert statement. + * @return the generated binding + */ + public String getRecordBasedInsertBinding(BindableColumn column, String prefix, String parameterName) { + return getFormattedJdbcPlaceholder(column, prefix, parameterName); + } + + /** + * This method generates a binding for a parameter to a placeholder in a row based insert statement. + * + *

This binding is specifically for use with insert, batch insert, and multirow insert statements and the + * MapToRow mapping. These statements bind parameters to the row class directly. + * + *

For MyBatis, a binding looks like this: "#{parameterName,jdbcType=xxx,typeHandler=xxx,javaType=xxx}" + * + *

For Spring, a binding looks like this: ":parameterName" + * + * @param column column definition used for generating type details in a MyBatis binding. Ignored for Spring. + * @param parameterName name of the parameter. Typically, will be + * either "row" or "records[x]" to match the properties of the generated statement object class. + * @return the generated binding + */ + public abstract String getRecordBasedInsertBinding(BindableColumn column, String parameterName); } diff --git a/src/main/java/org/mybatis/dynamic/sql/render/SpringNamedParameterRenderingStrategy.java b/src/main/java/org/mybatis/dynamic/sql/render/SpringNamedParameterRenderingStrategy.java index 9037d4f46..ccdbae51f 100644 --- a/src/main/java/org/mybatis/dynamic/sql/render/SpringNamedParameterRenderingStrategy.java +++ b/src/main/java/org/mybatis/dynamic/sql/render/SpringNamedParameterRenderingStrategy.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -18,7 +18,7 @@ import org.mybatis.dynamic.sql.BindableColumn; public class SpringNamedParameterRenderingStrategy extends RenderingStrategy { - + @Override public String getFormattedJdbcPlaceholder(BindableColumn column, String prefix, String parameterName) { return getFormattedJdbcPlaceholder(prefix, parameterName); @@ -28,4 +28,14 @@ public String getFormattedJdbcPlaceholder(BindableColumn column, String prefi public String getFormattedJdbcPlaceholder(String prefix, String parameterName) { return ":" + parameterName; //$NON-NLS-1$ } + + @Override + public String getRecordBasedInsertBinding(BindableColumn column, String prefix, String parameterName) { + return ":" + prefix + "." + parameterName; //$NON-NLS-1$ //$NON-NLS-2$ + } + + @Override + public String getRecordBasedInsertBinding(BindableColumn column, String parameterName) { + return ":" + parameterName; //$NON-NLS-1$ + } } diff --git a/src/main/java/org/mybatis/dynamic/sql/render/TableAliasCalculator.java b/src/main/java/org/mybatis/dynamic/sql/render/TableAliasCalculator.java index ea9feedd4..a90d337f2 100644 --- a/src/main/java/org/mybatis/dynamic/sql/render/TableAliasCalculator.java +++ b/src/main/java/org/mybatis/dynamic/sql/render/TableAliasCalculator.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,41 +15,27 @@ */ package org.mybatis.dynamic.sql.render; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; import java.util.Optional; import org.mybatis.dynamic.sql.SqlTable; -public class TableAliasCalculator { - - private Map aliases; +public interface TableAliasCalculator { - protected TableAliasCalculator(Map aliases) { - this.aliases = Objects.requireNonNull(aliases); - } - - public Optional aliasForColumn(SqlTable table) { - return Optional.ofNullable(aliases.get(table)); - } + Optional aliasForColumn(SqlTable table); - public Optional aliasForTable(SqlTable table) { - return Optional.ofNullable(aliases.get(table)); - } - - public static TableAliasCalculator of(SqlTable table, String alias) { - Map tableAliases = new HashMap<>(); - tableAliases.put(table, alias); - return of(tableAliases); - } - - public static TableAliasCalculator of(Map aliases) { - return new TableAliasCalculator(aliases); - } - - public static TableAliasCalculator empty() { - return of(Collections.emptyMap()); + Optional aliasForTable(SqlTable table); + + static TableAliasCalculator empty() { + return new TableAliasCalculator() { + @Override + public Optional aliasForColumn(SqlTable table) { + return table.tableAlias(); + } + + @Override + public Optional aliasForTable(SqlTable table) { + return table.tableAlias(); + } + }; } } diff --git a/src/main/java/org/mybatis/dynamic/sql/render/TableAliasCalculatorWithParent.java b/src/main/java/org/mybatis/dynamic/sql/render/TableAliasCalculatorWithParent.java new file mode 100644 index 000000000..e843751d3 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/render/TableAliasCalculatorWithParent.java @@ -0,0 +1,69 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.render; + +import java.util.Objects; +import java.util.Optional; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.SqlTable; + +public class TableAliasCalculatorWithParent implements TableAliasCalculator { + private final TableAliasCalculator parent; + private final TableAliasCalculator child; + + private TableAliasCalculatorWithParent(Builder builder) { + parent = Objects.requireNonNull(builder.parent); + child = Objects.requireNonNull(builder.child); + } + + @Override + public Optional aliasForColumn(SqlTable table) { + Optional answer = child.aliasForColumn(table); + if (answer.isPresent()) { + return answer; + } + return parent.aliasForColumn(table); + } + + @Override + public Optional aliasForTable(SqlTable table) { + Optional answer = child.aliasForTable(table); + if (answer.isPresent()) { + return answer; + } + return parent.aliasForTable(table); + } + + public static class Builder { + private @Nullable TableAliasCalculator parent; + private @Nullable TableAliasCalculator child; + + public Builder withParent(TableAliasCalculator parent) { + this.parent = parent; + return this; + } + + public Builder withChild(TableAliasCalculator child) { + this.child = child; + return this; + } + + public TableAliasCalculatorWithParent build() { + return new TableAliasCalculatorWithParent(this); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/render/package-info.java b/src/main/java/org/mybatis/dynamic/sql/render/package-info.java new file mode 100644 index 000000000..770ff3d47 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/render/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +@NullMarked +package org.mybatis.dynamic.sql.render; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/mybatis/dynamic/sql/select/AbstractHavingFinisher.java b/src/main/java/org/mybatis/dynamic/sql/select/AbstractHavingFinisher.java new file mode 100644 index 000000000..872b964eb --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/AbstractHavingFinisher.java @@ -0,0 +1,42 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select; + +import java.util.List; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.AndOrCriteriaGroup; +import org.mybatis.dynamic.sql.SqlCriterion; +import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionDSL; + +public abstract class AbstractHavingFinisher> + extends AbstractBooleanExpressionDSL { + void initialize(SqlCriterion sqlCriterion) { + setInitialCriterion(sqlCriterion, StatementType.HAVING); + } + + void initialize(@Nullable SqlCriterion sqlCriterion, List subCriteria) { + setInitialCriterion(sqlCriterion, StatementType.HAVING); + super.subCriteria.addAll(subCriteria); + } + + protected HavingModel buildModel() { + return new HavingModel.Builder() + .withInitialCriterion(getInitialCriterion()) + .withSubCriteria(subCriteria) + .build(); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/AbstractHavingStarter.java b/src/main/java/org/mybatis/dynamic/sql/select/AbstractHavingStarter.java new file mode 100644 index 000000000..1090fb064 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/AbstractHavingStarter.java @@ -0,0 +1,71 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select; + +import java.util.Arrays; +import java.util.List; + +import org.mybatis.dynamic.sql.AndOrCriteriaGroup; +import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.ColumnAndConditionCriterion; +import org.mybatis.dynamic.sql.CriteriaGroup; +import org.mybatis.dynamic.sql.RenderableCondition; +import org.mybatis.dynamic.sql.SqlCriterion; + +public interface AbstractHavingStarter> { + + default F having(BindableColumn column, RenderableCondition condition, + AndOrCriteriaGroup... subCriteria) { + return having(column, condition, Arrays.asList(subCriteria)); + } + + default F having(BindableColumn column, RenderableCondition condition, + List subCriteria) { + SqlCriterion sqlCriterion = ColumnAndConditionCriterion.withColumn(column) + .withCondition(condition) + .withSubCriteria(subCriteria) + .build(); + + return initialize(sqlCriterion); + } + + default F having(SqlCriterion initialCriterion, AndOrCriteriaGroup... subCriteria) { + return having(initialCriterion, Arrays.asList(subCriteria)); + } + + default F having(SqlCriterion initialCriterion, List subCriteria) { + SqlCriterion sqlCriterion = new CriteriaGroup.Builder() + .withInitialCriterion(initialCriterion) + .withSubCriteria(subCriteria) + .build(); + + return initialize(sqlCriterion); + } + + F having(); + + default F applyHaving(HavingApplier havingApplier) { + F finisher = having(); + havingApplier.accept(finisher); + return finisher; + } + + private F initialize(SqlCriterion sqlCriterion) { + F finisher = having(); + finisher.initialize(sqlCriterion); + return finisher; + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/AbstractQueryExpressionDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/AbstractQueryExpressionDSL.java index b115d755c..f1fd826a8 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/AbstractQueryExpressionDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/AbstractQueryExpressionDSL.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -17,153 +17,214 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.stream.Collectors; +import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.AndOrCriteriaGroup; +import org.mybatis.dynamic.sql.SqlCriterion; import org.mybatis.dynamic.sql.SqlTable; -import org.mybatis.dynamic.sql.select.join.JoinCriterion; +import org.mybatis.dynamic.sql.TableExpression; +import org.mybatis.dynamic.sql.exception.DuplicateTableAliasException; import org.mybatis.dynamic.sql.select.join.JoinModel; import org.mybatis.dynamic.sql.select.join.JoinSpecification; import org.mybatis.dynamic.sql.select.join.JoinType; import org.mybatis.dynamic.sql.util.Buildable; +import org.mybatis.dynamic.sql.where.AbstractWhereFinisher; +import org.mybatis.dynamic.sql.where.AbstractWhereStarter; -public abstract class AbstractQueryExpressionDSL, R> - implements Buildable { +public abstract class AbstractQueryExpressionDSL, + T extends AbstractQueryExpressionDSL> + implements AbstractWhereStarter { - private List joinSpecificationBuilders = new ArrayList<>(); - protected Map tableAliases = new HashMap<>(); - private SqlTable table; - - protected AbstractQueryExpressionDSL(SqlTable table) { + private final List> joinSpecificationSuppliers = new ArrayList<>(); + private final Map tableAliases = new HashMap<>(); + private final TableExpression table; + + protected AbstractQueryExpressionDSL(TableExpression table) { this.table = Objects.requireNonNull(table); } - - public SqlTable table() { + + public TableExpression table() { return table; } - - public T join(SqlTable joinTable, JoinCriterion onJoinCriterion, - JoinCriterion...andJoinCriteria) { - addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.INNER, Arrays.asList(andJoinCriteria)); + + public T join(SqlTable joinTable, SqlCriterion onJoinCriterion, + AndOrCriteriaGroup... andJoinCriteria) { + addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.INNER, Arrays.asList(andJoinCriteria)); return getThis(); } - - public T join(SqlTable joinTable, String tableAlias, JoinCriterion onJoinCriterion, - JoinCriterion...andJoinCriteria) { - tableAliases.put(joinTable, tableAlias); + + public T join(SqlTable joinTable, String tableAlias, SqlCriterion onJoinCriterion, + AndOrCriteriaGroup... andJoinCriteria) { + addTableAlias(joinTable, tableAlias); return join(joinTable, onJoinCriterion, andJoinCriteria); } - - public T join(SqlTable joinTable, JoinCriterion onJoinCriterion, - List andJoinCriteria) { - addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.INNER, andJoinCriteria); + + public T join(SqlTable joinTable, @Nullable SqlCriterion onJoinCriterion, + List andJoinCriteria) { + addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.INNER, andJoinCriteria); return getThis(); } - - public T join(SqlTable joinTable, String tableAlias, JoinCriterion onJoinCriterion, - List andJoinCriteria) { - tableAliases.put(joinTable, tableAlias); + + public T join(SqlTable joinTable, String tableAlias, @Nullable SqlCriterion onJoinCriterion, + List andJoinCriteria) { + addTableAlias(joinTable, tableAlias); return join(joinTable, onJoinCriterion, andJoinCriteria); } - - public T leftJoin(SqlTable joinTable, JoinCriterion onJoinCriterion, - JoinCriterion...andJoinCriteria) { - addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.LEFT, Arrays.asList(andJoinCriteria)); + + public T join(Buildable subQuery, @Nullable String tableAlias, @Nullable SqlCriterion onJoinCriterion, + List andJoinCriteria) { + addJoinSpecificationSupplier(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.INNER, + andJoinCriteria); return getThis(); } - - public T leftJoin(SqlTable joinTable, String tableAlias, JoinCriterion onJoinCriterion, - JoinCriterion...andJoinCriteria) { - tableAliases.put(joinTable, tableAlias); + + public T leftJoin(SqlTable joinTable, SqlCriterion onJoinCriterion, + AndOrCriteriaGroup... andJoinCriteria) { + addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.LEFT, Arrays.asList(andJoinCriteria)); + return getThis(); + } + + public T leftJoin(SqlTable joinTable, String tableAlias, SqlCriterion onJoinCriterion, + AndOrCriteriaGroup... andJoinCriteria) { + addTableAlias(joinTable, tableAlias); return leftJoin(joinTable, onJoinCriterion, andJoinCriteria); } - - public T leftJoin(SqlTable joinTable, JoinCriterion onJoinCriterion, - List andJoinCriteria) { - addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.LEFT, andJoinCriteria); + + public T leftJoin(SqlTable joinTable, @Nullable SqlCriterion onJoinCriterion, + List andJoinCriteria) { + addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.LEFT, andJoinCriteria); return getThis(); } - - public T leftJoin(SqlTable joinTable, String tableAlias, JoinCriterion onJoinCriterion, - List andJoinCriteria) { - tableAliases.put(joinTable, tableAlias); + + public T leftJoin(SqlTable joinTable, String tableAlias, @Nullable SqlCriterion onJoinCriterion, + List andJoinCriteria) { + addTableAlias(joinTable, tableAlias); return leftJoin(joinTable, onJoinCriterion, andJoinCriteria); } - - public T rightJoin(SqlTable joinTable, JoinCriterion onJoinCriterion, - JoinCriterion...andJoinCriteria) { - addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.RIGHT, Arrays.asList(andJoinCriteria)); + + public T leftJoin(Buildable subQuery, @Nullable String tableAlias, + @Nullable SqlCriterion onJoinCriterion, List andJoinCriteria) { + addJoinSpecificationSupplier(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.LEFT, + andJoinCriteria); + return getThis(); + } + + public T rightJoin(SqlTable joinTable, SqlCriterion onJoinCriterion, + AndOrCriteriaGroup... andJoinCriteria) { + addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.RIGHT, Arrays.asList(andJoinCriteria)); return getThis(); } - - public T rightJoin(SqlTable joinTable, String tableAlias, JoinCriterion onJoinCriterion, - JoinCriterion...andJoinCriteria) { - tableAliases.put(joinTable, tableAlias); + + public T rightJoin(SqlTable joinTable, String tableAlias, SqlCriterion onJoinCriterion, + AndOrCriteriaGroup... andJoinCriteria) { + addTableAlias(joinTable, tableAlias); return rightJoin(joinTable, onJoinCriterion, andJoinCriteria); } - public T rightJoin(SqlTable joinTable, JoinCriterion onJoinCriterion, - List andJoinCriteria) { - addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.RIGHT, andJoinCriteria); + public T rightJoin(SqlTable joinTable, @Nullable SqlCriterion onJoinCriterion, + List andJoinCriteria) { + addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.RIGHT, andJoinCriteria); return getThis(); } - - public T rightJoin(SqlTable joinTable, String tableAlias, JoinCriterion onJoinCriterion, - List andJoinCriteria) { - tableAliases.put(joinTable, tableAlias); + + public T rightJoin(SqlTable joinTable, String tableAlias, @Nullable SqlCriterion onJoinCriterion, + List andJoinCriteria) { + addTableAlias(joinTable, tableAlias); return rightJoin(joinTable, onJoinCriterion, andJoinCriteria); } - public T fullJoin(SqlTable joinTable, JoinCriterion onJoinCriterion, - JoinCriterion...andJoinCriteria) { - addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.FULL, Arrays.asList(andJoinCriteria)); + public T rightJoin(Buildable subQuery, @Nullable String tableAlias, + @Nullable SqlCriterion onJoinCriterion, List andJoinCriteria) { + addJoinSpecificationSupplier(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.RIGHT, + andJoinCriteria); return getThis(); } - - public T fullJoin(SqlTable joinTable, String tableAlias, JoinCriterion onJoinCriterion, - JoinCriterion...andJoinCriteria) { - tableAliases.put(joinTable, tableAlias); + + public T fullJoin(SqlTable joinTable, SqlCriterion onJoinCriterion, + AndOrCriteriaGroup... andJoinCriteria) { + addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.FULL, Arrays.asList(andJoinCriteria)); + return getThis(); + } + + public T fullJoin(SqlTable joinTable, String tableAlias, SqlCriterion onJoinCriterion, + AndOrCriteriaGroup... andJoinCriteria) { + addTableAlias(joinTable, tableAlias); return fullJoin(joinTable, onJoinCriterion, andJoinCriteria); } - public T fullJoin(SqlTable joinTable, JoinCriterion onJoinCriterion, - List andJoinCriteria) { - addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.FULL, andJoinCriteria); + public T fullJoin(SqlTable joinTable, @Nullable SqlCriterion onJoinCriterion, + List andJoinCriteria) { + addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.FULL, andJoinCriteria); return getThis(); } - - public T fullJoin(SqlTable joinTable, String tableAlias, JoinCriterion onJoinCriterion, - List andJoinCriteria) { - tableAliases.put(joinTable, tableAlias); + + public T fullJoin(SqlTable joinTable, String tableAlias, @Nullable SqlCriterion onJoinCriterion, + List andJoinCriteria) { + addTableAlias(joinTable, tableAlias); return fullJoin(joinTable, onJoinCriterion, andJoinCriteria); } - private void addJoinSpecificationBuilder(SqlTable joinTable, JoinCriterion onJoinCriterion, JoinType joinType, - List andJoinCriteria) { - joinSpecificationBuilders.add(new JoinSpecification.Builder() + public T fullJoin(Buildable subQuery, @Nullable String tableAlias, + @Nullable SqlCriterion onJoinCriterion, List andJoinCriteria) { + addJoinSpecificationSupplier(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.FULL, + andJoinCriteria); + return getThis(); + } + + private void addJoinSpecificationSupplier(TableExpression joinTable, @Nullable SqlCriterion onJoinCriterion, + JoinType joinType, List andJoinCriteria) { + joinSpecificationSuppliers.add(() -> new JoinSpecification.Builder() .withJoinTable(joinTable) .withJoinType(joinType) - .withJoinCriterion(onJoinCriterion) - .withJoinCriteria(andJoinCriteria)); + .withInitialCriterion(onJoinCriterion) + .withSubCriteria(andJoinCriteria).build()); } - - protected void addJoinSpecificationBuilder(JoinSpecification.Builder builder) { - joinSpecificationBuilders.add(builder); + + protected void addJoinSpecificationSupplier(Supplier joinSpecificationSupplier) { + joinSpecificationSuppliers.add(joinSpecificationSupplier); } - + protected Optional buildJoinModel() { - if (joinSpecificationBuilders.isEmpty()) { + if (joinSpecificationSuppliers.isEmpty()) { return Optional.empty(); } - - return Optional.of(JoinModel.of(joinSpecificationBuilders.stream() - .map(JoinSpecification.Builder::build) - .collect(Collectors.toList()))); + + return Optional.of(JoinModel.of(joinSpecificationSuppliers.stream() + .map(Supplier::get) + .toList())); + } + + protected void addTableAlias(SqlTable table, String tableAlias) { + if (tableAliases.containsKey(table)) { + throw new DuplicateTableAliasException(table, tableAlias, tableAliases.get(table)); + } + + tableAliases.put(table, tableAlias); + } + + protected Map tableAliases() { + return Collections.unmodifiableMap(tableAliases); } - + + protected static SubQuery buildSubQuery(Buildable selectModel) { + return new SubQuery.Builder() + .withSelectModel(selectModel.build()) + .build(); + } + + protected static SubQuery buildSubQuery(Buildable selectModel, @Nullable String alias) { + return new SubQuery.Builder() + .withSelectModel(selectModel.build()) + .withAlias(alias) + .build(); + } + protected abstract T getThis(); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/AbstractSelectModel.java b/src/main/java/org/mybatis/dynamic/sql/select/AbstractSelectModel.java new file mode 100644 index 000000000..51c3d4fe7 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/AbstractSelectModel.java @@ -0,0 +1,70 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select; + +import java.util.Objects; +import java.util.Optional; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.common.OrderByModel; +import org.mybatis.dynamic.sql.configuration.StatementConfiguration; + +public abstract class AbstractSelectModel { + private final @Nullable OrderByModel orderByModel; + private final @Nullable PagingModel pagingModel; + protected final StatementConfiguration statementConfiguration; + + protected AbstractSelectModel(AbstractBuilder builder) { + orderByModel = builder.orderByModel; + pagingModel = builder.pagingModel; + statementConfiguration = Objects.requireNonNull(builder.statementConfiguration); + } + + public Optional orderByModel() { + return Optional.ofNullable(orderByModel); + } + + public Optional pagingModel() { + return Optional.ofNullable(pagingModel); + } + + public StatementConfiguration statementConfiguration() { + return statementConfiguration; + } + + public abstract static class AbstractBuilder> { + private @Nullable OrderByModel orderByModel; + private @Nullable PagingModel pagingModel; + private @Nullable StatementConfiguration statementConfiguration; + + public T withOrderByModel(@Nullable OrderByModel orderByModel) { + this.orderByModel = orderByModel; + return getThis(); + } + + public T withPagingModel(@Nullable PagingModel pagingModel) { + this.pagingModel = pagingModel; + return getThis(); + } + + public T withStatementConfiguration(StatementConfiguration statementConfiguration) { + this.statementConfiguration = statementConfiguration; + return getThis(); + } + + protected abstract T getThis(); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/ColumnSortSpecification.java b/src/main/java/org/mybatis/dynamic/sql/select/ColumnSortSpecification.java new file mode 100644 index 000000000..aa74099b5 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/ColumnSortSpecification.java @@ -0,0 +1,49 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select; + +import java.util.Objects; + +import org.mybatis.dynamic.sql.SortSpecification; +import org.mybatis.dynamic.sql.SqlColumn; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + +public class ColumnSortSpecification implements SortSpecification { + private final String tableAlias; + private final SqlColumn column; + private final String descendingPhrase; + + public ColumnSortSpecification(String tableAlias, SqlColumn column) { + this(tableAlias, column, ""); //$NON-NLS-1$ + } + + private ColumnSortSpecification(String tableAlias, SqlColumn column, String descendingPhrase) { + this.tableAlias = Objects.requireNonNull(tableAlias); + this.column = Objects.requireNonNull(column); + this.descendingPhrase = descendingPhrase; + } + + @Override + public SortSpecification descending() { + return new ColumnSortSpecification(tableAlias, column, " DESC"); //$NON-NLS-1$ + } + + @Override + public FragmentAndParameters renderForOrderBy(RenderingContext renderingContext) { + return FragmentAndParameters.fromFragment(tableAlias + "." + column.name() + descendingPhrase); //$NON-NLS-1$ + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/CountDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/CountDSL.java index 00f25b307..48e790a03 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/CountDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/CountDSL.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -16,100 +16,134 @@ package org.mybatis.dynamic.sql.select; import java.util.Objects; +import java.util.function.Consumer; import java.util.function.Function; -import org.mybatis.dynamic.sql.BindableColumn; +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.SqlBuilder; -import org.mybatis.dynamic.sql.SqlCriterion; import org.mybatis.dynamic.sql.SqlTable; -import org.mybatis.dynamic.sql.VisitableCondition; +import org.mybatis.dynamic.sql.configuration.StatementConfiguration; import org.mybatis.dynamic.sql.util.Buildable; -import org.mybatis.dynamic.sql.where.AbstractWhereDSL; -import org.mybatis.dynamic.sql.where.WhereApplier; -import org.mybatis.dynamic.sql.where.WhereModel; +import org.mybatis.dynamic.sql.where.AbstractWhereFinisher; +import org.mybatis.dynamic.sql.where.EmbeddedWhereModel; /** * DSL for building count queries. Count queries are specializations of select queries. They have joins and where * clauses, but not the other parts of a select (group by, order by, etc.) Count queries always return * a long. If these restrictions are not acceptable, then use the Select DSL for an unrestricted select statement. * - * @param the type of model built by this Builder. Typically SelectModel. + * @param the type of model built by this Builder. Typically, SelectModel. * * @author Jeff Butler */ -public class CountDSL extends AbstractQueryExpressionDSL, R> implements Buildable { +public class CountDSL extends AbstractQueryExpressionDSL.CountWhereBuilder, CountDSL> + implements Buildable { + + private final Function adapterFunction; + private @Nullable CountWhereBuilder whereBuilder; + private final BasicColumn countColumn; + private final StatementConfiguration statementConfiguration = new StatementConfiguration(); - private Function adapterFunction; - private CountWhereBuilder whereBuilder = new CountWhereBuilder(); - - private CountDSL(SqlTable table, Function adapterFunction) { + private CountDSL(BasicColumn countColumn, SqlTable table, Function adapterFunction) { super(table); + this.countColumn = Objects.requireNonNull(countColumn); this.adapterFunction = Objects.requireNonNull(adapterFunction); } - - public CountWhereBuilder where() { - return whereBuilder; - } - public CountWhereBuilder where(BindableColumn column, VisitableCondition condition, - SqlCriterion...subCriteria) { - whereBuilder.where(column, condition, subCriteria); + @Override + public CountWhereBuilder where() { + whereBuilder = Objects.requireNonNullElseGet(whereBuilder, CountWhereBuilder::new); return whereBuilder; } - public CountWhereBuilder applyWhere(WhereApplier whereApplier) { - return whereBuilder.applyWhere(whereApplier); - } - @Override public R build() { return adapterFunction.apply(buildModel()); } + @Override + public CountDSL configureStatement(Consumer consumer) { + consumer.accept(statementConfiguration); + return this; + } + private SelectModel buildModel() { - QueryExpressionModel.Builder b = new QueryExpressionModel.Builder() - .withSelectColumn(SqlBuilder.count()) + QueryExpressionModel queryExpressionModel = new QueryExpressionModel.Builder() + .withSelectColumn(countColumn) .withTable(table()) - .withTableAliases(tableAliases) - .withWhereModel(whereBuilder.buildWhereModel()); - - buildJoinModel().ifPresent(b::withJoinModel); - + .withTableAliases(tableAliases()) + .withJoinModel(buildJoinModel().orElse(null)) + .withWhereModel(whereBuilder == null ? null : whereBuilder.buildWhereModel()) + .build(); + return new SelectModel.Builder() - .withQueryExpression(b.build()) + .withQueryExpression(queryExpressionModel) + .withStatementConfiguration(statementConfiguration) .build(); } - + public static CountDSL countFrom(SqlTable table) { return countFrom(Function.identity(), table); } - + public static CountDSL countFrom(Function adapterFunction, SqlTable table) { - return new CountDSL<>(table, adapterFunction); + return new CountDSL<>(SqlBuilder.count(), table, adapterFunction); + } + + public static FromGatherer count(BasicColumn column) { + return count(Function.identity(), column); + } + + public static FromGatherer count(Function adapterFunction, BasicColumn column) { + return new FromGatherer<>(adapterFunction, SqlBuilder.count(column)); + } + + public static FromGatherer countDistinct(BasicColumn column) { + return countDistinct(Function.identity(), column); } - + + public static FromGatherer countDistinct(Function adapterFunction, BasicColumn column) { + return new FromGatherer<>(adapterFunction, SqlBuilder.countDistinct(column)); + } + @Override protected CountDSL getThis() { return this; } - - public class CountWhereBuilder extends AbstractWhereDSL + + public static class FromGatherer { + private final BasicColumn column; + private final Function adapterFunction; + + public FromGatherer(Function adapterFunction, BasicColumn column) { + this.adapterFunction = adapterFunction; + this.column = column; + } + + public CountDSL from(SqlTable table) { + return new CountDSL<>(column, table, adapterFunction); + } + } + + public class CountWhereBuilder extends AbstractWhereFinisher implements Buildable { - private CountWhereBuilder() {} + private CountWhereBuilder() { + super(CountDSL.this); + } @Override public R build() { return CountDSL.this.build(); } - + @Override protected CountWhereBuilder getThis() { return this; } - @Override - protected WhereModel buildWhereModel() { - return super.internalBuild(); + protected EmbeddedWhereModel buildWhereModel() { + return super.buildModel(); } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/CountDSLCompleter.java b/src/main/java/org/mybatis/dynamic/sql/select/CountDSLCompleter.java index 60cc2c353..d2972d26f 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/CountDSLCompleter.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/CountDSLCompleter.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -37,7 +37,7 @@ * * default long count(CountDSLCompleter completer) { * return MyBatis3Utils.count(this::count, person, completer); - * } + * } * * *

And then call the simplified default method like this: @@ -64,10 +64,10 @@ @FunctionalInterface public interface CountDSLCompleter extends Function, Buildable> { - + /** * Returns a completer that can be used to count every row in a table. - * + * * @return the completer that will count every row in a table */ static CountDSLCompleter allRows() { diff --git a/src/main/java/org/mybatis/dynamic/sql/select/GroupByModel.java b/src/main/java/org/mybatis/dynamic/sql/select/GroupByModel.java index 41fb5dde9..4c85dcd45 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/GroupByModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/GroupByModel.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -16,25 +16,28 @@ package org.mybatis.dynamic.sql.select; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collection; import java.util.List; -import java.util.function.Function; +import java.util.Objects; import java.util.stream.Stream; import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.util.Validator; public class GroupByModel { - private List columns = new ArrayList<>(); - - private GroupByModel(List columns) { + private final List columns = new ArrayList<>(); + + private GroupByModel(Collection columns) { + Objects.requireNonNull(columns); + Validator.assertNotEmpty(columns, "ERROR.11"); //$NON-NLS-1$ this.columns.addAll(columns); } - - public Stream mapColumns(Function mapper) { - return columns.stream().map(mapper); + + public Stream columns() { + return columns.stream(); } - - public static GroupByModel of(BasicColumn...columns) { - return new GroupByModel(Arrays.asList(columns)); + + public static GroupByModel of(Collection columns) { + return new GroupByModel(columns); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/HavingApplier.java b/src/main/java/org/mybatis/dynamic/sql/select/HavingApplier.java new file mode 100644 index 000000000..d0a61a7fb --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/HavingApplier.java @@ -0,0 +1,40 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select; + +import java.util.function.Consumer; + +import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionDSL; + +@FunctionalInterface +public interface HavingApplier { + + void accept(AbstractHavingFinisher havingFinisher); + + /** + * Return a composed having applier that performs this operation followed by the after operation. + * + * @param after the operation to perform after this operation + * + * @return a composed having applier that performs this operation followed by the after operation. + */ + default HavingApplier andThen(Consumer> after) { + return t -> { + accept(t); + after.accept(t); + }; + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/HavingDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/HavingDSL.java new file mode 100644 index 000000000..58ca01184 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/HavingDSL.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select; + +import org.mybatis.dynamic.sql.util.Buildable; + +public class HavingDSL implements AbstractHavingStarter { + private final StandaloneHavingFinisher havingFinisher = new StandaloneHavingFinisher(); + + @Override + public StandaloneHavingFinisher having() { + return havingFinisher; + } + + public static class StandaloneHavingFinisher extends AbstractHavingFinisher + implements Buildable { + + private StandaloneHavingFinisher() {} + + @Override + protected StandaloneHavingFinisher getThis() { + return this; + } + + @Override + public HavingModel build() { + return buildModel(); + } + + public HavingApplier toHavingApplier() { + return d -> d.initialize(getInitialCriterion(), subCriteria); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/HavingModel.java b/src/main/java/org/mybatis/dynamic/sql/select/HavingModel.java new file mode 100644 index 000000000..5f8cd38c8 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/HavingModel.java @@ -0,0 +1,35 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select; + +import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionModel; + +public class HavingModel extends AbstractBooleanExpressionModel { + private HavingModel(Builder builder) { + super(builder); + } + + public static class Builder extends AbstractBuilder { + public HavingModel build() { + return new HavingModel(this); + } + + @Override + protected Builder getThis() { + return this; + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/MultiSelectDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/MultiSelectDSL.java new file mode 100644 index 000000000..124fe095b --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/MultiSelectDSL.java @@ -0,0 +1,153 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.SortSpecification; +import org.mybatis.dynamic.sql.common.OrderByModel; +import org.mybatis.dynamic.sql.configuration.StatementConfiguration; +import org.mybatis.dynamic.sql.util.Buildable; +import org.mybatis.dynamic.sql.util.ConfigurableStatement; + +public class MultiSelectDSL implements Buildable, ConfigurableStatement { + private final List unionQueries = new ArrayList<>(); + private final SelectModel initialSelect; + private @Nullable OrderByModel orderByModel; + private @Nullable Long limit; + private @Nullable Long offset; + private @Nullable Long fetchFirstRows; + private final StatementConfiguration statementConfiguration = new StatementConfiguration(); + + public MultiSelectDSL(Buildable builder) { + initialSelect = builder.build(); + } + + public MultiSelectDSL union(Buildable builder) { + unionQueries.add(new UnionQuery("union", builder.build())); //$NON-NLS-1$ + return this; + } + + public MultiSelectDSL unionAll(Buildable builder) { + unionQueries.add(new UnionQuery("union all", builder.build())); //$NON-NLS-1$ + return this; + } + + public MultiSelectDSL orderBy(SortSpecification... columns) { + return orderBy(Arrays.asList(columns)); + } + + public MultiSelectDSL orderBy(Collection columns) { + orderByModel = OrderByModel.of(columns); + return this; + } + + public MultiSelectDSL.LimitFinisher limit(long limit) { + return limitWhenPresent(limit); + } + + public MultiSelectDSL.LimitFinisher limitWhenPresent(@Nullable Long limit) { + this.limit = limit; + return new LimitFinisher(); + } + + public MultiSelectDSL.OffsetFirstFinisher offset(long offset) { + return offsetWhenPresent(offset); + } + + public MultiSelectDSL.OffsetFirstFinisher offsetWhenPresent(@Nullable Long offset) { + this.offset = offset; + return new OffsetFirstFinisher(); + } + + public MultiSelectDSL.FetchFirstFinisher fetchFirst(long fetchFirstRows) { + return fetchFirstWhenPresent(fetchFirstRows); + } + + public MultiSelectDSL.FetchFirstFinisher fetchFirstWhenPresent(@Nullable Long fetchFirstRows) { + this.fetchFirstRows = fetchFirstRows; + return new FetchFirstFinisher(); + } + + @Override + public MultiSelectModel build() { + return new MultiSelectModel.Builder() + .withInitialSelect(initialSelect) + .withUnionQueries(unionQueries) + .withOrderByModel(orderByModel) + .withPagingModel(buildPagingModel().orElse(null)) + .withStatementConfiguration(statementConfiguration) + .build(); + } + + private Optional buildPagingModel() { + return new PagingModel.Builder() + .withLimit(limit) + .withOffset(offset) + .withFetchFirstRows(fetchFirstRows) + .build(); + } + + @Override + public MultiSelectDSL configureStatement(Consumer consumer) { + consumer.accept(statementConfiguration); + return this; + } + + public class OffsetFirstFinisher implements Buildable { + public FetchFirstFinisher fetchFirst(long fetchFirstRows) { + return fetchFirstWhenPresent(fetchFirstRows); + } + + public FetchFirstFinisher fetchFirstWhenPresent(@Nullable Long fetchFirstRows) { + MultiSelectDSL.this.fetchFirstRows = fetchFirstRows; + return new FetchFirstFinisher(); + } + + @Override + public MultiSelectModel build() { + return MultiSelectDSL.this.build(); + } + } + + public class LimitFinisher implements Buildable { + public MultiSelectDSL offset(long offset) { + return offsetWhenPresent(offset); + } + + public MultiSelectDSL offsetWhenPresent(@Nullable Long offset) { + MultiSelectDSL.this.offset = offset; + return MultiSelectDSL.this; + } + + @Override + public MultiSelectModel build() { + return MultiSelectDSL.this.build(); + } + } + + public class FetchFirstFinisher { + public MultiSelectDSL rowsOnly() { + return MultiSelectDSL.this; + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/MultiSelectModel.java b/src/main/java/org/mybatis/dynamic/sql/select/MultiSelectModel.java new file mode 100644 index 000000000..94dbe765b --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/MultiSelectModel.java @@ -0,0 +1,78 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.render.RenderingStrategy; +import org.mybatis.dynamic.sql.select.render.MultiSelectRenderer; +import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; +import org.mybatis.dynamic.sql.util.Validator; + +public class MultiSelectModel extends AbstractSelectModel { + private final SelectModel initialSelect; + private final List unionQueries; + + private MultiSelectModel(Builder builder) { + super(builder); + initialSelect = Objects.requireNonNull(builder.initialSelect); + unionQueries = builder.unionQueries; + Validator.assertNotEmpty(unionQueries, "ERROR.35"); //$NON-NLS-1$ + } + + public SelectModel initialSelect() { + return initialSelect; + } + + public Stream unionQueries() { + return unionQueries.stream(); + } + + public SelectStatementProvider render(RenderingStrategy renderingStrategy) { + return MultiSelectRenderer.withMultiSelectModel(this) + .withRenderingStrategy(renderingStrategy) + .build() + .render(); + } + + public static class Builder extends AbstractBuilder { + private @Nullable SelectModel initialSelect; + private final List unionQueries = new ArrayList<>(); + + public Builder withInitialSelect(SelectModel initialSelect) { + this.initialSelect = initialSelect; + return this; + } + + public Builder withUnionQueries(List unionQueries) { + this.unionQueries.addAll(unionQueries); + return this; + } + + @Override + protected Builder getThis() { + return this; + } + + public MultiSelectModel build() { + return new MultiSelectModel(this); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/MyBatis3SelectModelAdapter.java b/src/main/java/org/mybatis/dynamic/sql/select/MyBatis3SelectModelAdapter.java deleted file mode 100644 index baf72eadb..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/select/MyBatis3SelectModelAdapter.java +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright 2016-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.select; - -import java.util.Objects; -import java.util.function.Function; - -import org.mybatis.dynamic.sql.render.RenderingStrategy; -import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; - -/** - * This adapter will render the underlying select model for MyBatis3, and then call a MyBatis mapper method. - * - * @deprecated in favor is {@link SelectDSLCompleter}. This class will be removed without direct replacement - * in a future version - * - * @author Jeff Butler - * - */ -@Deprecated -public class MyBatis3SelectModelAdapter { - - private SelectModel selectModel; - private Function mapperMethod; - - private MyBatis3SelectModelAdapter(SelectModel selectModel, Function mapperMethod) { - this.selectModel = Objects.requireNonNull(selectModel); - this.mapperMethod = Objects.requireNonNull(mapperMethod); - } - - public R execute() { - return mapperMethod.apply(selectStatement()); - } - - private SelectStatementProvider selectStatement() { - return selectModel.render(RenderingStrategy.MYBATIS3); - } - - public static MyBatis3SelectModelAdapter of(SelectModel selectModel, - Function mapperMethod) { - return new MyBatis3SelectModelAdapter<>(selectModel, mapperMethod); - } -} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/PagingModel.java b/src/main/java/org/mybatis/dynamic/sql/select/PagingModel.java index d43967b55..b5da5e01c 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/PagingModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/PagingModel.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -17,11 +17,13 @@ import java.util.Optional; +import org.jspecify.annotations.Nullable; + public class PagingModel { - private Long limit; - private Long offset; - private Long fetchFirstRows; + private final @Nullable Long limit; + private final @Nullable Long offset; + private final @Nullable Long fetchFirstRows; private PagingModel(Builder builder) { super(); @@ -41,29 +43,33 @@ public Optional offset() { public Optional fetchFirstRows() { return Optional.ofNullable(fetchFirstRows); } - + public static class Builder { - private Long limit; - private Long offset; - private Long fetchFirstRows; + private @Nullable Long limit; + private @Nullable Long offset; + private @Nullable Long fetchFirstRows; - public Builder withLimit(Long limit) { + public Builder withLimit(@Nullable Long limit) { this.limit = limit; return this; } - - public Builder withOffset(Long offset) { + + public Builder withOffset(@Nullable Long offset) { this.offset = offset; return this; } - - public Builder withFetchFirstRows(Long fetchFirstRows) { + + public Builder withFetchFirstRows(@Nullable Long fetchFirstRows) { this.fetchFirstRows = fetchFirstRows; return this; } - - public PagingModel build() { - return new PagingModel(this); + + public Optional build() { + if (limit == null && offset == null && fetchFirstRows == null) { + return Optional.empty(); + } + + return Optional.of(new PagingModel(this)); } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java index 1a32f1fc8..70ad2617d 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -20,57 +20,82 @@ import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.AndOrCriteriaGroup; import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.ColumnAndConditionCriterion; +import org.mybatis.dynamic.sql.CriteriaGroup; +import org.mybatis.dynamic.sql.RenderableCondition; import org.mybatis.dynamic.sql.SortSpecification; -import org.mybatis.dynamic.sql.SqlCriterion; import org.mybatis.dynamic.sql.SqlTable; -import org.mybatis.dynamic.sql.VisitableCondition; -import org.mybatis.dynamic.sql.select.join.JoinCondition; -import org.mybatis.dynamic.sql.select.join.JoinCriterion; +import org.mybatis.dynamic.sql.TableExpression; +import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionDSL; +import org.mybatis.dynamic.sql.configuration.StatementConfiguration; import org.mybatis.dynamic.sql.select.join.JoinSpecification; import org.mybatis.dynamic.sql.select.join.JoinType; import org.mybatis.dynamic.sql.util.Buildable; -import org.mybatis.dynamic.sql.where.AbstractWhereDSL; -import org.mybatis.dynamic.sql.where.WhereApplier; -import org.mybatis.dynamic.sql.where.WhereModel; - -public class QueryExpressionDSL extends AbstractQueryExpressionDSL, R> - implements Buildable { - - private String connector; - private SelectDSL selectDSL; - private boolean isDistinct; - private List selectList; - private QueryExpressionWhereBuilder whereBuilder = new QueryExpressionWhereBuilder(); - private GroupByModel groupByModel; - - QueryExpressionDSL(FromGatherer fromGatherer) { - super(fromGatherer.table); +import org.mybatis.dynamic.sql.where.AbstractWhereFinisher; +import org.mybatis.dynamic.sql.where.AbstractWhereStarter; +import org.mybatis.dynamic.sql.where.EmbeddedWhereModel; + +public class QueryExpressionDSL + extends AbstractQueryExpressionDSL.QueryExpressionWhereBuilder, QueryExpressionDSL> + implements Buildable, SelectDSLOperations { + + private final @Nullable String connector; + private final SelectDSL selectDSL; + private final boolean isDistinct; + private final List selectList; + private @Nullable QueryExpressionWhereBuilder whereBuilder; + private @Nullable GroupByModel groupByModel; + private @Nullable QueryExpressionHavingBuilder havingBuilder; + + protected QueryExpressionDSL(FromGatherer fromGatherer, TableExpression table) { + super(table); connector = fromGatherer.connector; selectList = fromGatherer.selectList; isDistinct = fromGatherer.isDistinct; selectDSL = Objects.requireNonNull(fromGatherer.selectDSL); + selectDSL.registerQueryExpression(this); } - - QueryExpressionDSL(FromGatherer fromGatherer, String tableAlias) { - this(fromGatherer); - tableAliases.put(fromGatherer.table, tableAlias); + + protected QueryExpressionDSL(FromGatherer fromGatherer, SqlTable table, String tableAlias) { + this(fromGatherer, table); + addTableAlias(table, tableAlias); } - + + @Override public QueryExpressionWhereBuilder where() { + whereBuilder = Objects.requireNonNullElseGet(whereBuilder, QueryExpressionWhereBuilder::new); return whereBuilder; } - public QueryExpressionWhereBuilder where(BindableColumn column, VisitableCondition condition, - SqlCriterion...subCriteria) { - whereBuilder.where(column, condition, subCriteria); - return whereBuilder; + @Override + public QueryExpressionDSL configureStatement(Consumer consumer) { + selectDSL.configureStatement(consumer); + return this; + } + + /** + * This method is protected here because it doesn't make sense at this point in the DSL. + * + * @return The having builder + */ + protected QueryExpressionHavingBuilder having() { + havingBuilder = Objects.requireNonNullElseGet(havingBuilder, QueryExpressionHavingBuilder::new); + return havingBuilder; } - public QueryExpressionWhereBuilder applyWhere(WhereApplier whereApplier) { - return whereBuilder.applyWhere(whereApplier); + /** + * This method is meant for use by the Kotlin DSL. We expect a full set of criteria. + * + * @param criteriaGroup the full criteria for a Kotlin Having clause + */ + protected void applyHaving(CriteriaGroup criteriaGroup) { + having().initialize(criteriaGroup); } @Override @@ -81,45 +106,69 @@ public R build() { public JoinSpecificationStarter join(SqlTable joinTable) { return new JoinSpecificationStarter(joinTable, JoinType.INNER); } - + public JoinSpecificationStarter join(SqlTable joinTable, String tableAlias) { - tableAliases.put(joinTable, tableAlias); + addTableAlias(joinTable, tableAlias); return join(joinTable); } + public JoinSpecificationStarter join(Buildable joinTable, String tableAlias) { + return new JoinSpecificationStarter(buildSubQuery(joinTable, tableAlias), JoinType.INNER); + } + public JoinSpecificationStarter leftJoin(SqlTable joinTable) { return new JoinSpecificationStarter(joinTable, JoinType.LEFT); } - + public JoinSpecificationStarter leftJoin(SqlTable joinTable, String tableAlias) { - tableAliases.put(joinTable, tableAlias); + addTableAlias(joinTable, tableAlias); return leftJoin(joinTable); } + public JoinSpecificationStarter leftJoin(Buildable joinTable, String tableAlias) { + return new JoinSpecificationStarter(buildSubQuery(joinTable, tableAlias), JoinType.LEFT); + } + public JoinSpecificationStarter rightJoin(SqlTable joinTable) { return new JoinSpecificationStarter(joinTable, JoinType.RIGHT); } - + public JoinSpecificationStarter rightJoin(SqlTable joinTable, String tableAlias) { - tableAliases.put(joinTable, tableAlias); + addTableAlias(joinTable, tableAlias); return rightJoin(joinTable); } + public JoinSpecificationStarter rightJoin(Buildable joinTable, String tableAlias) { + return new JoinSpecificationStarter(buildSubQuery(joinTable, tableAlias), JoinType.RIGHT); + } + public JoinSpecificationStarter fullJoin(SqlTable joinTable) { return new JoinSpecificationStarter(joinTable, JoinType.FULL); } - + public JoinSpecificationStarter fullJoin(SqlTable joinTable, String tableAlias) { - tableAliases.put(joinTable, tableAlias); + addTableAlias(joinTable, tableAlias); return fullJoin(joinTable); } - public GroupByFinisher groupBy(BasicColumn...columns) { + public JoinSpecificationStarter fullJoin(Buildable joinTable, String tableAlias) { + return new JoinSpecificationStarter(buildSubQuery(joinTable, tableAlias), JoinType.FULL); + } + + public GroupByFinisher groupBy(BasicColumn... columns) { + return groupBy(Arrays.asList(columns)); + } + + public GroupByFinisher groupBy(Collection columns) { groupByModel = GroupByModel.of(columns); return new GroupByFinisher(); } - - public SelectDSL orderBy(SortSpecification...columns) { + + public SelectDSL orderBy(SortSpecification... columns) { + return orderBy(Arrays.asList(columns)); + } + + public SelectDSL orderBy(Collection columns) { selectDSL.orderBy(columns); return selectDSL; } @@ -137,66 +186,65 @@ protected QueryExpressionModel buildModel() { .withConnector(connector) .withTable(table()) .isDistinct(isDistinct) - .withTableAliases(tableAliases) - .withWhereModel(whereBuilder.buildWhereModel()) + .withTableAliases(tableAliases()) .withJoinModel(buildJoinModel().orElse(null)) .withGroupByModel(groupByModel) + .withWhereModel(whereBuilder == null ? null : whereBuilder.buildWhereModel()) + .withHavingModel(havingBuilder == null ? null : havingBuilder.buildHavingModel()) .build(); } - - public SelectDSL.LimitFinisher limit(long limit) { - return selectDSL.limit(limit); - } - - public SelectDSL.OffsetFirstFinisher offset(long offset) { - return selectDSL.offset(offset); - } - public SelectDSL.FetchFirstFinisher fetchFirst(long fetchFirstRows) { - return selectDSL.fetchFirst(fetchFirstRows); - } - @Override protected QueryExpressionDSL getThis() { return this; } - + + @Override + public SelectDSL getSelectDSL() { + return selectDSL; + } + public static class FromGatherer { - private String connector; - private List selectList; - private SelectDSL selectDSL; - private boolean isDistinct; - private SqlTable table; - + private final @Nullable String connector; + private final List selectList; + private final SelectDSL selectDSL; + private final boolean isDistinct; + public FromGatherer(Builder builder) { this.connector = builder.connector; - this.selectList = Objects.requireNonNull(builder.selectList); + this.selectList = builder.selectList; this.selectDSL = Objects.requireNonNull(builder.selectDSL); this.isDistinct = builder.isDistinct; } - + + public QueryExpressionDSL from(Buildable select) { + return new QueryExpressionDSL<>(this, buildSubQuery(select)); + } + + public QueryExpressionDSL from(Buildable select, String tableAlias) { + return new QueryExpressionDSL<>(this, buildSubQuery(select, tableAlias)); + } + public QueryExpressionDSL from(SqlTable table) { - this.table = table; - return selectDSL.newQueryExpression(this); + return new QueryExpressionDSL<>(this, table); } public QueryExpressionDSL from(SqlTable table, String tableAlias) { - this.table = table; - return selectDSL.newQueryExpression(this, tableAlias); + return new QueryExpressionDSL<>(this, table, tableAlias); } - + public static class Builder { - private String connector; - private List selectList = new ArrayList<>(); - private SelectDSL selectDSL; + private @Nullable String connector; + private final List selectList = new ArrayList<>(); + private @Nullable SelectDSL selectDSL; private boolean isDistinct; - + public Builder withConnector(String connector) { this.connector = connector; return this; } - public Builder withSelectList(Collection selectList) { + public Builder withSelectList(Collection selectList) { this.selectList.addAll(selectList); return this; } @@ -205,21 +253,22 @@ public Builder withSelectDSL(SelectDSL selectDSL) { this.selectDSL = selectDSL; return this; } - + public Builder isDistinct() { this.isDistinct = true; return this; } - + public FromGatherer build() { return new FromGatherer<>(this); } } } - - public class QueryExpressionWhereBuilder extends AbstractWhereDSL - implements Buildable { - private QueryExpressionWhereBuilder() { + + public class QueryExpressionWhereBuilder extends AbstractWhereFinisher + implements Buildable, SelectDSLOperations { + private QueryExpressionWhereBuilder() { + super(QueryExpressionDSL.this); } public UnionBuilder union() { @@ -230,159 +279,177 @@ public UnionBuilder unionAll() { return QueryExpressionDSL.this.unionAll(); } - public SelectDSL orderBy(SortSpecification...columns) { - return QueryExpressionDSL.this.orderBy(columns); + public SelectDSL orderBy(SortSpecification... columns) { + return orderBy(Arrays.asList(columns)); } - - public GroupByFinisher groupBy(BasicColumn...columns) { - return QueryExpressionDSL.this.groupBy(columns); - } - - public SelectDSL.LimitFinisher limit(long limit) { - return QueryExpressionDSL.this.limit(limit); + + public SelectDSL orderBy(Collection columns) { + return QueryExpressionDSL.this.orderBy(columns); } - - public SelectDSL.OffsetFirstFinisher offset(long offset) { - return QueryExpressionDSL.this.offset(offset); + + public GroupByFinisher groupBy(BasicColumn... columns) { + return groupBy(Arrays.asList(columns)); } - - public SelectDSL.FetchFirstFinisher fetchFirst(long fetchFirstRows) { - return QueryExpressionDSL.this.fetchFirst(fetchFirstRows); + + public GroupByFinisher groupBy(Collection columns) { + return QueryExpressionDSL.this.groupBy(columns); } - + @Override public R build() { return QueryExpressionDSL.this.build(); } - + @Override protected QueryExpressionWhereBuilder getThis() { return this; } @Override - protected WhereModel buildWhereModel() { - return super.internalBuild(); + public SelectDSL getSelectDSL() { + return QueryExpressionDSL.this.getSelectDSL(); + } + + protected EmbeddedWhereModel buildWhereModel() { + return super.buildModel(); } } - + public class JoinSpecificationStarter { - private SqlTable joinTable; - private JoinType joinType; - - public JoinSpecificationStarter(SqlTable joinTable, JoinType joinType) { + private final TableExpression joinTable; + private final JoinType joinType; + + public JoinSpecificationStarter(TableExpression joinTable, JoinType joinType) { this.joinTable = joinTable; this.joinType = joinType; } - public JoinSpecificationFinisher on(BasicColumn joinColumn, JoinCondition joinCondition) { + public JoinSpecificationFinisher on(BindableColumn joinColumn, RenderableCondition joinCondition) { return new JoinSpecificationFinisher(joinTable, joinColumn, joinCondition, joinType); } - public JoinSpecificationFinisher on(BasicColumn joinColumn, JoinCondition onJoinCondition, - JoinCriterion...andJoinCriteria) { - return new JoinSpecificationFinisher(joinTable, joinColumn, onJoinCondition, joinType, andJoinCriteria); + public JoinSpecificationFinisher on(BindableColumn joinColumn, RenderableCondition onJoinCondition, + AndOrCriteriaGroup... subCriteria) { + return new JoinSpecificationFinisher(joinTable, joinColumn, onJoinCondition, joinType, subCriteria); } } - public class JoinSpecificationFinisher implements Buildable { - private JoinSpecification.Builder joinSpecificationBuilder; - - public JoinSpecificationFinisher(SqlTable table, BasicColumn joinColumn, - JoinCondition joinCondition, JoinType joinType) { - JoinCriterion joinCriterion = new JoinCriterion.Builder() - .withConnector("on") //$NON-NLS-1$ - .withJoinColumn(joinColumn) - .withJoinCondition(joinCondition) - .build(); + public class JoinSpecificationFinisher + extends AbstractBooleanExpressionDSL + implements AbstractWhereStarter, Buildable, + SelectDSLOperations { - joinSpecificationBuilder = JoinSpecification.withJoinTable(table) - .withJoinType(joinType) - .withJoinCriterion(joinCriterion); + private final TableExpression table; + private final JoinType joinType; + + public JoinSpecificationFinisher(TableExpression table, BindableColumn joinColumn, + RenderableCondition joinCondition, JoinType joinType) { + this.table = table; + this.joinType = joinType; + addJoinSpecificationSupplier(this::buildJoinSpecification); + + ColumnAndConditionCriterion criterion = ColumnAndConditionCriterion.withColumn(joinColumn) + .withCondition(joinCondition) + .build(); - addJoinSpecificationBuilder(joinSpecificationBuilder); + setInitialCriterion(criterion); } - public JoinSpecificationFinisher(SqlTable table, BasicColumn joinColumn, - JoinCondition joinCondition, JoinType joinType, JoinCriterion...andJoinCriteria) { - JoinCriterion onJoinCriterion = new JoinCriterion.Builder() - .withConnector("on") //$NON-NLS-1$ - .withJoinColumn(joinColumn) - .withJoinCondition(joinCondition) + public JoinSpecificationFinisher(TableExpression table, BindableColumn joinColumn, + RenderableCondition joinCondition, JoinType joinType, + AndOrCriteriaGroup... subCriteria) { + this.table = table; + this.joinType = joinType; + addJoinSpecificationSupplier(this::buildJoinSpecification); + + ColumnAndConditionCriterion criterion = ColumnAndConditionCriterion.withColumn(joinColumn) + .withCondition(joinCondition) + .withSubCriteria(Arrays.asList(subCriteria)) .build(); - - joinSpecificationBuilder = JoinSpecification.withJoinTable(table) - .withJoinType(joinType) - .withJoinCriterion(onJoinCriterion) - .withJoinCriteria(Arrays.asList(andJoinCriteria)); - addJoinSpecificationBuilder(joinSpecificationBuilder); + setInitialCriterion(criterion); } - + + private JoinSpecification buildJoinSpecification() { + return JoinSpecification.withJoinTable(table) + .withJoinType(joinType) + .withInitialCriterion(getInitialCriterion()) + .withSubCriteria(subCriteria) + .build(); + } + @Override public R build() { return QueryExpressionDSL.this.build(); } - - public QueryExpressionWhereBuilder where() { - return QueryExpressionDSL.this.where(); - } - - public QueryExpressionWhereBuilder where(BindableColumn column, VisitableCondition condition, - SqlCriterion...subCriteria) { - return QueryExpressionDSL.this.where(column, condition, subCriteria); - } - public QueryExpressionWhereBuilder applyWhere(WhereApplier whereApplier) { - return QueryExpressionDSL.this.applyWhere(whereApplier); - } - - public JoinSpecificationFinisher and(BasicColumn joinColumn, JoinCondition joinCondition) { - JoinCriterion joinCriterion = new JoinCriterion.Builder() - .withConnector("and") //$NON-NLS-1$ - .withJoinColumn(joinColumn) - .withJoinCondition(joinCondition) - .build(); - joinSpecificationBuilder.withJoinCriterion(joinCriterion); + @Override + public JoinSpecificationFinisher configureStatement(Consumer consumer) { + selectDSL.configureStatement(consumer); return this; } + @Override + public QueryExpressionWhereBuilder where() { + return QueryExpressionDSL.this.where(); + } + public JoinSpecificationStarter join(SqlTable joinTable) { return QueryExpressionDSL.this.join(joinTable); } - + public JoinSpecificationStarter join(SqlTable joinTable, String tableAlias) { return QueryExpressionDSL.this.join(joinTable, tableAlias); } + public JoinSpecificationStarter join(Buildable joinTable, String tableAlias) { + return QueryExpressionDSL.this.join(joinTable, tableAlias); + } + public JoinSpecificationStarter leftJoin(SqlTable joinTable) { return QueryExpressionDSL.this.leftJoin(joinTable); } - + public JoinSpecificationStarter leftJoin(SqlTable joinTable, String tableAlias) { return QueryExpressionDSL.this.leftJoin(joinTable, tableAlias); } + public JoinSpecificationStarter leftJoin(Buildable joinTable, String tableAlias) { + return QueryExpressionDSL.this.leftJoin(joinTable, tableAlias); + } + public JoinSpecificationStarter rightJoin(SqlTable joinTable) { return QueryExpressionDSL.this.rightJoin(joinTable); } - + public JoinSpecificationStarter rightJoin(SqlTable joinTable, String tableAlias) { return QueryExpressionDSL.this.rightJoin(joinTable, tableAlias); } + public JoinSpecificationStarter rightJoin(Buildable joinTable, String tableAlias) { + return QueryExpressionDSL.this.rightJoin(joinTable, tableAlias); + } + public JoinSpecificationStarter fullJoin(SqlTable joinTable) { return QueryExpressionDSL.this.fullJoin(joinTable); } - + public JoinSpecificationStarter fullJoin(SqlTable joinTable, String tableAlias) { return QueryExpressionDSL.this.fullJoin(joinTable, tableAlias); } - public GroupByFinisher groupBy(BasicColumn...columns) { + public JoinSpecificationStarter fullJoin(Buildable joinTable, String tableAlias) { + return QueryExpressionDSL.this.fullJoin(joinTable, tableAlias); + } + + public GroupByFinisher groupBy(BasicColumn... columns) { + return groupBy(Arrays.asList(columns)); + } + + public GroupByFinisher groupBy(Collection columns) { return QueryExpressionDSL.this.groupBy(columns); } - + public UnionBuilder union() { return QueryExpressionDSL.this.union(); } @@ -391,25 +458,32 @@ public UnionBuilder unionAll() { return QueryExpressionDSL.this.unionAll(); } - public SelectDSL orderBy(SortSpecification...columns) { - return QueryExpressionDSL.this.orderBy(columns); + public SelectDSL orderBy(SortSpecification... columns) { + return orderBy(Arrays.asList(columns)); } - public SelectDSL.LimitFinisher limit(long limit) { - return QueryExpressionDSL.this.limit(limit); + public SelectDSL orderBy(Collection columns) { + return QueryExpressionDSL.this.orderBy(columns); } - public SelectDSL.OffsetFirstFinisher offset(long offset) { - return QueryExpressionDSL.this.offset(offset); + @Override + protected JoinSpecificationFinisher getThis() { + return this; } - public SelectDSL.FetchFirstFinisher fetchFirst(long fetchFirstRows) { - return QueryExpressionDSL.this.fetchFirst(fetchFirstRows); + @Override + public SelectDSL getSelectDSL() { + return QueryExpressionDSL.this.getSelectDSL(); } } - - public class GroupByFinisher implements Buildable { - public SelectDSL orderBy(SortSpecification...columns) { + + public class GroupByFinisher implements AbstractHavingStarter, + Buildable, SelectDSLOperations { + public SelectDSL orderBy(SortSpecification... columns) { + return orderBy(Arrays.asList(columns)); + } + + public SelectDSL orderBy(Collection columns) { return QueryExpressionDSL.this.orderBy(columns); } @@ -417,36 +491,34 @@ public SelectDSL orderBy(SortSpecification...columns) { public R build() { return QueryExpressionDSL.this.build(); } - + public UnionBuilder union() { return QueryExpressionDSL.this.union(); } - + public UnionBuilder unionAll() { return QueryExpressionDSL.this.unionAll(); } - - public SelectDSL.LimitFinisher limit(long limit) { - return QueryExpressionDSL.this.limit(limit); - } - public SelectDSL.OffsetFirstFinisher offset(long offset) { - return QueryExpressionDSL.this.offset(offset); + @Override + public QueryExpressionHavingBuilder having() { + return QueryExpressionDSL.this.having(); } - public SelectDSL.FetchFirstFinisher fetchFirst(long fetchFirstRows) { - return QueryExpressionDSL.this.fetchFirst(fetchFirstRows); + @Override + public SelectDSL getSelectDSL() { + return QueryExpressionDSL.this.getSelectDSL(); } } - + public class UnionBuilder { - protected String connector; - + protected final String connector; + public UnionBuilder(String connector) { this.connector = connector; } - - public FromGatherer select(BasicColumn...selectList) { + + public FromGatherer select(BasicColumn... selectList) { return select(Arrays.asList(selectList)); } @@ -458,7 +530,7 @@ public FromGatherer select(List selectList) { .build(); } - public FromGatherer selectDistinct(BasicColumn...selectList) { + public FromGatherer selectDistinct(BasicColumn... selectList) { return selectDistinct(Arrays.asList(selectList)); } @@ -471,4 +543,43 @@ public FromGatherer selectDistinct(List selectList) { .build(); } } + + public class QueryExpressionHavingBuilder extends AbstractHavingFinisher + implements Buildable, SelectDSLOperations { + + public SelectDSL orderBy(SortSpecification... columns) { + return orderBy(Arrays.asList(columns)); + } + + public SelectDSL orderBy(Collection columns) { + return QueryExpressionDSL.this.orderBy(columns); + } + + public UnionBuilder union() { + return QueryExpressionDSL.this.union(); + } + + public UnionBuilder unionAll() { + return QueryExpressionDSL.this.unionAll(); + } + + @Override + public R build() { + return QueryExpressionDSL.this.build(); + } + + @Override + protected QueryExpressionHavingBuilder getThis() { + return this; + } + + protected HavingModel buildHavingModel() { + return super.buildModel(); + } + + @Override + public SelectDSL getSelectDSL() { + return QueryExpressionDSL.this.getSelectDSL(); + } + } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionModel.java b/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionModel.java index 3561b5b51..4561c0781 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionModel.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,33 +15,32 @@ */ package org.mybatis.dynamic.sql.select; -import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore; - import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.function.Function; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.SqlTable; -import org.mybatis.dynamic.sql.render.GuaranteedTableAliasCalculator; -import org.mybatis.dynamic.sql.render.TableAliasCalculator; +import org.mybatis.dynamic.sql.TableExpression; import org.mybatis.dynamic.sql.select.join.JoinModel; -import org.mybatis.dynamic.sql.where.WhereModel; +import org.mybatis.dynamic.sql.util.Validator; +import org.mybatis.dynamic.sql.where.EmbeddedWhereModel; public class QueryExpressionModel { - private String connector; - private boolean isDistinct; - private List selectList; - private SqlTable table; - private JoinModel joinModel; - private TableAliasCalculator tableAliasCalculator; - private WhereModel whereModel; - private GroupByModel groupByModel; + private final @Nullable String connector; + private final boolean isDistinct; + private final List selectList; + private final TableExpression table; + private final @Nullable JoinModel joinModel; + private final Map tableAliases; + private final @Nullable EmbeddedWhereModel whereModel; + private final @Nullable GroupByModel groupByModel; + private final @Nullable HavingModel havingModel; private QueryExpressionModel(Builder builder) { connector = builder.connector; @@ -49,73 +48,74 @@ private QueryExpressionModel(Builder builder) { selectList = Objects.requireNonNull(builder.selectList); table = Objects.requireNonNull(builder.table); joinModel = builder.joinModel; - tableAliasCalculator = joinModel().map(jm -> GuaranteedTableAliasCalculator.of(builder.tableAliases)) - .orElseGet(() -> TableAliasCalculator.of(builder.tableAliases)); + tableAliases = builder.tableAliases; whereModel = builder.whereModel; groupByModel = builder.groupByModel; + havingModel = builder.havingModel; + Validator.assertNotEmpty(selectList, "ERROR.13"); //$NON-NLS-1$ } - + public Optional connector() { return Optional.ofNullable(connector); } - + public boolean isDistinct() { return isDistinct; } - - public Stream mapColumns(Function mapper) { - return selectList.stream().map(mapper); + + public Stream columns() { + return selectList.stream(); } - - public SqlTable table() { + + public TableExpression table() { return table; } - - public TableAliasCalculator tableAliasCalculator() { - return tableAliasCalculator; + + public Map tableAliases() { + return tableAliases; } - public Optional whereModel() { + public Optional whereModel() { return Optional.ofNullable(whereModel); } - + public Optional joinModel() { return Optional.ofNullable(joinModel); } - + public Optional groupByModel() { return Optional.ofNullable(groupByModel); } - - public String calculateTableNameIncludingAlias(SqlTable table) { - return table.tableNameAtRuntime() - + spaceBefore(tableAliasCalculator.aliasForTable(table)); + + public Optional havingModel() { + return Optional.ofNullable(havingModel); } - - public static Builder withSelectList(List columnList) { + + public static Builder withSelectList(List columnList) { return new Builder().withSelectList(columnList); } - + public static class Builder { - private String connector; + private @Nullable String connector; private boolean isDistinct; - private List selectList = new ArrayList<>(); - private SqlTable table; - private Map tableAliases = new HashMap<>(); - private WhereModel whereModel; - private JoinModel joinModel; - private GroupByModel groupByModel; - - public Builder withConnector(String connector) { + private final List selectList = new ArrayList<>(); + private @Nullable TableExpression table; + private final Map tableAliases = new HashMap<>(); + private @Nullable EmbeddedWhereModel whereModel; + private @Nullable JoinModel joinModel; + private @Nullable GroupByModel groupByModel; + private @Nullable HavingModel havingModel; + + public Builder withConnector(@Nullable String connector) { this.connector = connector; return this; } - - public Builder withTable(SqlTable table) { + + public Builder withTable(TableExpression table) { this.table = table; return this; } - + public Builder isDistinct(boolean isDistinct) { this.isDistinct = isDistinct; return this; @@ -126,7 +126,7 @@ public Builder withSelectColumn(BasicColumn selectColumn) { return this; } - public Builder withSelectList(List selectList) { + public Builder withSelectList(List selectList) { this.selectList.addAll(selectList); return this; } @@ -135,22 +135,27 @@ public Builder withTableAliases(Map tableAliases) { this.tableAliases.putAll(tableAliases); return this; } - - public Builder withWhereModel(WhereModel whereModel) { + + public Builder withWhereModel(@Nullable EmbeddedWhereModel whereModel) { this.whereModel = whereModel; return this; } - public Builder withJoinModel(JoinModel joinModel) { + public Builder withJoinModel(@Nullable JoinModel joinModel) { this.joinModel = joinModel; return this; } - - public Builder withGroupByModel(GroupByModel groupByModel) { + + public Builder withGroupByModel(@Nullable GroupByModel groupByModel) { this.groupByModel = groupByModel; return this; } - + + public Builder withHavingModel(@Nullable HavingModel havingModel) { + this.havingModel = havingModel; + return this; + } + public QueryExpressionModel build() { return new QueryExpressionModel(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SelectDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/SelectDSL.java index 75b399b9f..2c2963772 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/SelectDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/SelectDSL.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -20,207 +20,236 @@ import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; import java.util.function.Function; -import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.SortSpecification; +import org.mybatis.dynamic.sql.common.OrderByModel; +import org.mybatis.dynamic.sql.configuration.StatementConfiguration; import org.mybatis.dynamic.sql.select.QueryExpressionDSL.FromGatherer; -import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; import org.mybatis.dynamic.sql.util.Buildable; -import org.mybatis.dynamic.sql.util.mybatis3.MyBatis3Utils; +import org.mybatis.dynamic.sql.util.ConfigurableStatement; +import org.mybatis.dynamic.sql.util.Validator; /** * Implements a SQL DSL for building select statements. - * + * * @author Jeff Butler * - * @param the type of model produced by this builder, typically SelectModel + * @param + * the type of model produced by this builder, typically SelectModel */ -public class SelectDSL implements Buildable { - - private Function adapterFunction; - private List> queryExpressions = new ArrayList<>(); - private OrderByModel orderByModel; - private Long limit; - private Long offset; - private Long fetchFirstRows; - +public class SelectDSL implements Buildable, ConfigurableStatement> { + + private final Function adapterFunction; + private final List> queryExpressions = new ArrayList<>(); + private @Nullable OrderByModel orderByModel; + private @Nullable Long limit; + private @Nullable Long offset; + private @Nullable Long fetchFirstRows; + final StatementConfiguration statementConfiguration = new StatementConfiguration(); + private @Nullable String forClause; + private @Nullable String waitClause; + private SelectDSL(Function adapterFunction) { this.adapterFunction = Objects.requireNonNull(adapterFunction); } - public static QueryExpressionDSL.FromGatherer select(BasicColumn...selectList) { + public static QueryExpressionDSL.FromGatherer select(BasicColumn... selectList) { return select(Arrays.asList(selectList)); } - - public static QueryExpressionDSL.FromGatherer select(Collection selectList) { + + public static QueryExpressionDSL.FromGatherer select(Collection selectList) { return select(Function.identity(), selectList); } - + public static QueryExpressionDSL.FromGatherer select(Function adapterFunction, - BasicColumn...selectList) { + BasicColumn... selectList) { return select(adapterFunction, Arrays.asList(selectList)); } - + public static QueryExpressionDSL.FromGatherer select(Function adapterFunction, - Collection selectList) { + Collection selectList) { return new FromGatherer.Builder() .withSelectList(selectList) .withSelectDSL(new SelectDSL<>(adapterFunction)) .build(); } - - public static QueryExpressionDSL.FromGatherer selectDistinct(BasicColumn...selectList) { - return selectDistinct(Arrays.asList(selectList)); + + public static QueryExpressionDSL.FromGatherer selectDistinct(BasicColumn... selectList) { + return selectDistinct(Function.identity(), selectList); } - - public static QueryExpressionDSL.FromGatherer selectDistinct(Collection selectList) { + + public static QueryExpressionDSL.FromGatherer selectDistinct( + Collection selectList) { return selectDistinct(Function.identity(), selectList); } - + public static QueryExpressionDSL.FromGatherer selectDistinct(Function adapterFunction, - BasicColumn...selectList) { + BasicColumn... selectList) { return selectDistinct(adapterFunction, Arrays.asList(selectList)); } - + public static QueryExpressionDSL.FromGatherer selectDistinct(Function adapterFunction, - Collection selectList) { + Collection selectList) { return new FromGatherer.Builder() .withSelectList(selectList) .withSelectDSL(new SelectDSL<>(adapterFunction)) .isDistinct() .build(); } - - /** - * Select records by executing a MyBatis3 Mapper. - * - * @deprecated in favor of various select methods in {@link MyBatis3Utils}. - * This method will be removed without direct replacement in a future version - * @param the return type from a MyBatis mapper - typically a List or a single record - * @param mapperMethod MyBatis3 mapper method that performs the select - * @param selectList the column list to select - * @return the partially created query - */ - @Deprecated - public static QueryExpressionDSL.FromGatherer> selectWithMapper( - Function mapperMethod, BasicColumn...selectList) { - return select(selectModel -> MyBatis3SelectModelAdapter.of(selectModel, mapperMethod), selectList); - } - - /** - * Select records by executing a MyBatis3 Mapper. - * - * @deprecated in favor of various select methods in {@link MyBatis3Utils}. - * This method will be removed without direct replacement in a future version - * @param the return type from a MyBatis mapper - typically a List or a single record - * @param mapperMethod MyBatis3 mapper method that performs the select - * @param selectList the column list to select - * @return the partially created query - */ - @Deprecated - public static QueryExpressionDSL.FromGatherer> selectDistinctWithMapper( - Function mapperMethod, BasicColumn...selectList) { - return selectDistinct(selectModel -> MyBatis3SelectModelAdapter.of(selectModel, mapperMethod), - selectList); - } - - QueryExpressionDSL newQueryExpression(FromGatherer fromGatherer) { - QueryExpressionDSL queryExpression = new QueryExpressionDSL<>(fromGatherer); - queryExpressions.add(queryExpression); - return queryExpression; - } - - QueryExpressionDSL newQueryExpression(FromGatherer fromGatherer, String tableAlias) { - QueryExpressionDSL queryExpression = new QueryExpressionDSL<>(fromGatherer, tableAlias); + + void registerQueryExpression(QueryExpressionDSL queryExpression) { queryExpressions.add(queryExpression); - return queryExpression; } - - void orderBy(SortSpecification...columns) { + + void orderBy(Collection columns) { orderByModel = OrderByModel.of(columns); } - - public LimitFinisher limit(long limit) { + + public SelectDSL.LimitFinisher limit(long limit) { + return limitWhenPresent(limit); + } + + public SelectDSL.LimitFinisher limitWhenPresent(@Nullable Long limit) { this.limit = limit; return new LimitFinisher(); } - public OffsetFirstFinisher offset(long offset) { + public SelectDSL.OffsetFirstFinisher offset(long offset) { + return offsetWhenPresent(offset); + } + + public SelectDSL.OffsetFirstFinisher offsetWhenPresent(@Nullable Long offset) { this.offset = offset; return new OffsetFirstFinisher(); } - public FetchFirstFinisher fetchFirst(long fetchFirstRows) { + public SelectDSL.FetchFirstFinisher fetchFirst(long fetchFirstRows) { + return fetchFirstWhenPresent(fetchFirstRows); + } + + public SelectDSL.FetchFirstFinisher fetchFirstWhenPresent(@Nullable Long fetchFirstRows) { this.fetchFirstRows = fetchFirstRows; return new FetchFirstFinisher(); } + public SelectDSL forUpdate() { + Validator.assertNull(forClause, "ERROR.48"); //$NON-NLS-1$ + forClause = "for update"; //$NON-NLS-1$ + return this; + } + + public SelectDSL forNoKeyUpdate() { + Validator.assertNull(forClause, "ERROR.48"); //$NON-NLS-1$ + forClause = "for no key update"; //$NON-NLS-1$ + return this; + } + + public SelectDSL forShare() { + Validator.assertNull(forClause, "ERROR.48"); //$NON-NLS-1$ + forClause = "for share"; //$NON-NLS-1$ + return this; + } + + public SelectDSL forKeyShare() { + Validator.assertNull(forClause, "ERROR.48"); //$NON-NLS-1$ + forClause = "for key share"; //$NON-NLS-1$ + return this; + } + + public SelectDSL skipLocked() { + Validator.assertNull(waitClause, "ERROR.49"); //$NON-NLS-1$ + waitClause = "skip locked"; //$NON-NLS-1$ + return this; + } + + public SelectDSL nowait() { + Validator.assertNull(waitClause, "ERROR.49"); //$NON-NLS-1$ + waitClause = "nowait"; //$NON-NLS-1$ + return this; + } + + @Override + public SelectDSL configureStatement(Consumer consumer) { + consumer.accept(statementConfiguration); + return this; + } + @Override public R build() { SelectModel selectModel = SelectModel.withQueryExpressions(buildModels()) .withOrderByModel(orderByModel) - .withPagingModel(buildPagingModel()) + .withPagingModel(buildPagingModel().orElse(null)) + .withStatementConfiguration(statementConfiguration) + .withForClause(forClause) + .withWaitClause(waitClause) .build(); return adapterFunction.apply(selectModel); } - + private List buildModels() { return queryExpressions.stream() .map(QueryExpressionDSL::buildModel) - .collect(Collectors.toList()); + .toList(); } - - private PagingModel buildPagingModel() { + + private Optional buildPagingModel() { return new PagingModel.Builder() .withLimit(limit) .withOffset(offset) .withFetchFirstRows(fetchFirstRows) .build(); } - - public class LimitFinisher implements Buildable { - public OffsetFinisher offset(long offset) { - SelectDSL.this.offset = offset; - return new OffsetFinisher(); + + public class OffsetFirstFinisher implements SelectDSLForAndWaitOperations, Buildable { + public FetchFirstFinisher fetchFirst(long fetchFirstRows) { + return fetchFirstWhenPresent(fetchFirstRows); } - + + public FetchFirstFinisher fetchFirstWhenPresent(@Nullable Long fetchFirstRows) { + SelectDSL.this.fetchFirstRows = fetchFirstRows; + return new FetchFirstFinisher(); + } + @Override - public R build() { - return SelectDSL.this.build(); + public SelectDSL getSelectDSL() { + return SelectDSL.this; } - } - public class OffsetFinisher implements Buildable { @Override public R build() { return SelectDSL.this.build(); } } - public class OffsetFirstFinisher implements Buildable { - public FetchFirstFinisher fetchFirst(long fetchFirstRows) { - SelectDSL.this.fetchFirstRows = fetchFirstRows; - return new FetchFirstFinisher(); + public class LimitFinisher implements SelectDSLForAndWaitOperations, Buildable { + public SelectDSL offset(long offset) { + return offsetWhenPresent(offset); + } + + public SelectDSL offsetWhenPresent(@Nullable Long offset) { + SelectDSL.this.offset = offset; + return SelectDSL.this; + } + + @Override + public SelectDSL getSelectDSL() { + return SelectDSL.this; } - + @Override public R build() { return SelectDSL.this.build(); } } - + public class FetchFirstFinisher { - public RowsOnlyFinisher rowsOnly() { - return new RowsOnlyFinisher(); - } - } - - public class RowsOnlyFinisher implements Buildable { - @Override - public R build() { - return SelectDSL.this.build(); + public SelectDSL rowsOnly() { + return SelectDSL.this; } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SelectDSLCompleter.java b/src/main/java/org/mybatis/dynamic/sql/select/SelectDSLCompleter.java index db20d0f73..a38d2d3cf 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/SelectDSLCompleter.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/SelectDSLCompleter.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -25,58 +25,58 @@ * Represents a function that can be used to create a general select method. When using this function, * you can create a method that does not require a user to call the build() and render() methods - making * client code look a bit cleaner. - * + * *

This function is intended to by used in conjunction with utility methods like the select methods in * {@link MyBatis3Utils}. - * + * *

For example, you can create mapper interface methods like this: - * + * *

  * @SelectProvider(type=SqlProviderAdapter.class, method="select")
  * List<PersonRecord> selectMany(SelectStatementProvider selectStatement);
- *   
+ *
  * BasicColumn[] selectList =
  *     BasicColumn.columnList(id, firstName, lastName, birthDate, employed, occupation, addressId);
- * 
+ *
  * default List<PersonRecord> select(SelectDSLCompleter completer) {
  *      return MyBatis3Utils.select(this::selectMany, selectList, person, completer);
  * }
  * 
- * + * *

And then call the simplified default method like this: - * + * *

  * List<PersonRecord> rows = mapper.select(c ->
  *         c.where(occupation, isNull()));
  * 
- * + * *

You can implement a "select all" with the following code: - * + * *

  * List<PersonRecord> rows = mapper.select(c -> c);
  * 
- * + * *

Or - * + * *

  * List<PersonRecord> rows = mapper.select(SelectDSLCompleter.allRows());
  * 
* *

There is also a utility method to support selecting all rows in a specified order: - * + * *

  * List<PersonRecord> rows = mapper.select(SelectDSLCompleter.allRowsOrderedBy(lastName, firstName));
  * 
- * + * * @author Jeff Butler */ @FunctionalInterface public interface SelectDSLCompleter extends Function, Buildable> { - + /** * Returns a completer that can be used to select every row in a table. - * + * * @return the completer that will select every row in a table */ static SelectDSLCompleter allRows() { @@ -85,11 +85,11 @@ static SelectDSLCompleter allRows() { /** * Returns a completer that can be used to select every row in a table with specified order. - * + * * @param columns list of sort specifications for an order by clause * @return the completer that will select every row in a table with specified order */ - static SelectDSLCompleter allRowsOrderedBy(SortSpecification...columns) { + static SelectDSLCompleter allRowsOrderedBy(SortSpecification... columns) { return c -> c.orderBy(columns); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SelectDSLForAndWaitOperations.java b/src/main/java/org/mybatis/dynamic/sql/select/SelectDSLForAndWaitOperations.java new file mode 100644 index 000000000..e4dc45d97 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/SelectDSLForAndWaitOperations.java @@ -0,0 +1,53 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select; + +public interface SelectDSLForAndWaitOperations { + default SelectDSL forUpdate() { + return getSelectDSL().forUpdate(); + } + + default SelectDSL forNoKeyUpdate() { + return getSelectDSL().forNoKeyUpdate(); + } + + default SelectDSL forShare() { + return getSelectDSL().forShare(); + } + + default SelectDSL forKeyShare() { + return getSelectDSL().forKeyShare(); + } + + default SelectDSL skipLocked() { + return getSelectDSL().skipLocked(); + } + + default SelectDSL nowait() { + return getSelectDSL().nowait(); + } + + /** + * Gain access to the SelectDSL instance. + * + *

This is a leak of an implementation detail into the public API. The tradeoff is that it + * significantly reduces copy/paste code of SelectDSL methods into all the different inner classes of + * QueryExpressionDSL where they would be needed. + * + * @return the SelectDSL instance associated with this interface instance + */ + SelectDSL getSelectDSL(); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SelectDSLOperations.java b/src/main/java/org/mybatis/dynamic/sql/select/SelectDSLOperations.java new file mode 100644 index 000000000..02f862c3b --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/SelectDSLOperations.java @@ -0,0 +1,44 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select; + +import org.jspecify.annotations.Nullable; + +public interface SelectDSLOperations extends SelectDSLForAndWaitOperations { + default SelectDSL.LimitFinisher limit(long limit) { + return getSelectDSL().limit(limit); + } + + default SelectDSL.LimitFinisher limitWhenPresent(@Nullable Long limit) { + return getSelectDSL().limitWhenPresent(limit); + } + + default SelectDSL.OffsetFirstFinisher offset(long offset) { + return getSelectDSL().offset(offset); + } + + default SelectDSL.OffsetFirstFinisher offsetWhenPresent(@Nullable Long offset) { + return getSelectDSL().offsetWhenPresent(offset); + } + + default SelectDSL.FetchFirstFinisher fetchFirst(long fetchFirstRows) { + return getSelectDSL().fetchFirst(fetchFirstRows); + } + + default SelectDSL.FetchFirstFinisher fetchFirstWhenPresent(@Nullable Long fetchFirstRows) { + return getSelectDSL().fetchFirstWhenPresent(fetchFirstRows); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SelectModel.java b/src/main/java/org/mybatis/dynamic/sql/select/SelectModel.java index 5e4c4c009..ec277760b 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/SelectModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/SelectModel.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -19,74 +19,80 @@ import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.function.Function; import java.util.stream.Stream; -import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.Nullable; import org.mybatis.dynamic.sql.render.RenderingStrategy; import org.mybatis.dynamic.sql.select.render.SelectRenderer; import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; +import org.mybatis.dynamic.sql.util.Validator; -public class SelectModel { - private List queryExpressions; - private OrderByModel orderByModel; - private PagingModel pagingModel; +public class SelectModel extends AbstractSelectModel { + private final List queryExpressions; + private final @Nullable String forClause; + private final @Nullable String waitClause; private SelectModel(Builder builder) { + super(builder); queryExpressions = Objects.requireNonNull(builder.queryExpressions); - orderByModel = builder.orderByModel; - pagingModel = builder.pagingModel; + Validator.assertNotEmpty(queryExpressions, "ERROR.14"); //$NON-NLS-1$ + forClause = builder.forClause; + waitClause = builder.waitClause; } - - public Stream mapQueryExpressions(Function mapper) { - return queryExpressions.stream().map(mapper); + + public Stream queryExpressions() { + return queryExpressions.stream(); } - - public Optional orderByModel() { - return Optional.ofNullable(orderByModel); + + public Optional forClause() { + return Optional.ofNullable(forClause); } - - public Optional pagingModel() { - return Optional.ofNullable(pagingModel); + + public Optional waitClause() { + return Optional.ofNullable(waitClause); } - @NotNull public SelectStatementProvider render(RenderingStrategy renderingStrategy) { return SelectRenderer.withSelectModel(this) .withRenderingStrategy(renderingStrategy) .build() .render(); } - + public static Builder withQueryExpressions(List queryExpressions) { return new Builder().withQueryExpressions(queryExpressions); } - - public static class Builder { - private List queryExpressions = new ArrayList<>(); - private OrderByModel orderByModel; - private PagingModel pagingModel; - + + public static class Builder extends AbstractBuilder { + private final List queryExpressions = new ArrayList<>(); + private @Nullable String forClause; + private @Nullable String waitClause; + public Builder withQueryExpression(QueryExpressionModel queryExpression) { this.queryExpressions.add(queryExpression); return this; } - + public Builder withQueryExpressions(List queryExpressions) { this.queryExpressions.addAll(queryExpressions); return this; } - - public Builder withOrderByModel(OrderByModel orderByModel) { - this.orderByModel = orderByModel; + + public Builder withForClause(@Nullable String forClause) { + this.forClause = forClause; + return this; + } + + public Builder withWaitClause(@Nullable String waitClause) { + this.waitClause = waitClause; return this; } - public Builder withPagingModel(PagingModel pagingModel) { - this.pagingModel = pagingModel; + @Override + protected Builder getThis() { return this; } - + public SelectModel build() { return new SelectModel(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SimpleSortSpecification.java b/src/main/java/org/mybatis/dynamic/sql/select/SimpleSortSpecification.java index 986c995b1..0a2c4918e 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/SimpleSortSpecification.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/SimpleSortSpecification.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -18,39 +18,39 @@ import java.util.Objects; import org.mybatis.dynamic.sql.SortSpecification; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; /** * This class is used for an order by phrase where there is no suitable column name - * to use (for example a calculated column or an aggregate column). - * + * to use (for example a calculated column or an aggregate column). + * * @author Jeff Butler */ public class SimpleSortSpecification implements SortSpecification { - - private String name; - private boolean isDescending; + + private final String name; + private final String descendingPhrase; private SimpleSortSpecification(String name) { + this(name, ""); //$NON-NLS-1$ + } + + private SimpleSortSpecification(String name, String descendingPhrase) { this.name = Objects.requireNonNull(name); + this.descendingPhrase = descendingPhrase; } - + @Override public SortSpecification descending() { - SimpleSortSpecification answer = new SimpleSortSpecification(name); - answer.isDescending = true; - return answer; + return new SimpleSortSpecification(name, " DESC"); //$NON-NLS-1$ } @Override - public String aliasOrName() { - return name; + public FragmentAndParameters renderForOrderBy(RenderingContext renderingContext) { + return FragmentAndParameters.fromFragment(name + descendingPhrase); } - @Override - public boolean isDescending() { - return isDescending; - } - public static SimpleSortSpecification of(String name) { return new SimpleSortSpecification(name); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SubQuery.java b/src/main/java/org/mybatis/dynamic/sql/select/SubQuery.java new file mode 100644 index 000000000..c2264c162 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/SubQuery.java @@ -0,0 +1,70 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select; + +import java.util.Objects; +import java.util.Optional; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.TableExpression; +import org.mybatis.dynamic.sql.TableExpressionVisitor; + +public class SubQuery implements TableExpression { + private final SelectModel selectModel; + private final @Nullable String alias; + + private SubQuery(Builder builder) { + selectModel = Objects.requireNonNull(builder.selectModel); + alias = builder.alias; + } + + public SelectModel selectModel() { + return selectModel; + } + + public Optional alias() { + return Optional.ofNullable(alias); + } + + @Override + public boolean isSubQuery() { + return true; + } + + @Override + public R accept(TableExpressionVisitor visitor) { + return visitor.visit(this); + } + + public static class Builder { + private @Nullable SelectModel selectModel; + private @Nullable String alias; + + public Builder withSelectModel(SelectModel selectModel) { + this.selectModel = selectModel; + return this; + } + + public Builder withAlias(@Nullable String alias) { + this.alias = alias; + return this; + } + + public SubQuery build() { + return new SubQuery(this); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/UnionQuery.java b/src/main/java/org/mybatis/dynamic/sql/select/UnionQuery.java new file mode 100644 index 000000000..6806d4791 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/UnionQuery.java @@ -0,0 +1,25 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select; + +import java.util.Objects; + +public record UnionQuery(String connector, SelectModel selectModel) { + public UnionQuery(String connector, SelectModel selectModel) { + this.connector = Objects.requireNonNull(connector); + this.selectModel = Objects.requireNonNull(selectModel); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/aggregate/AbstractAggregate.java b/src/main/java/org/mybatis/dynamic/sql/select/aggregate/AbstractAggregate.java deleted file mode 100644 index 240043b37..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/select/aggregate/AbstractAggregate.java +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright 2016-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.select.aggregate; - -import java.util.Objects; -import java.util.Optional; - -import org.mybatis.dynamic.sql.BasicColumn; -import org.mybatis.dynamic.sql.render.TableAliasCalculator; - -/** - * This class is the base class for aggregate functions. - * - * @author Jeff Butler - * - * @param the subclass type - */ -public abstract class AbstractAggregate> implements BasicColumn { - protected BasicColumn column; - protected String alias; - - protected AbstractAggregate(BasicColumn column) { - this.column = Objects.requireNonNull(column); - } - - @Override - public Optional alias() { - return Optional.ofNullable(alias); - } - - @Override - public String renderWithTableAlias(TableAliasCalculator tableAliasCalculator) { - return render(column.renderWithTableAlias(tableAliasCalculator)); - } - - @Override - public T as(String alias) { - T copy = copy(); - copy.alias = alias; - return copy; - } - - protected abstract T copy(); - - /** - * Calculate the rendered string for the select list. - * - * @param columnName the calculated column name. It will have the table alias already applied - * if applicable. - * @return the rendered string for the select list - */ - protected abstract String render(String columnName); -} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/aggregate/AbstractCount.java b/src/main/java/org/mybatis/dynamic/sql/select/aggregate/AbstractCount.java new file mode 100644 index 000000000..fe40329ea --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/aggregate/AbstractCount.java @@ -0,0 +1,43 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select.aggregate; + +import java.util.Optional; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.BindableColumn; + +/** + * Count functions are implemented differently than the other aggregates. This is primarily to preserve + * backwards compatibility. Count functions are configured as BindableColumns of type Long + * as it is assumed that the count functions always return a number. + */ +public abstract class AbstractCount implements BindableColumn { + private final @Nullable String alias; + + protected AbstractCount() { + this(null); + } + + protected AbstractCount(@Nullable String alias) { + this.alias = alias; + } + + @Override + public Optional alias() { + return Optional.ofNullable(alias); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/aggregate/Avg.java b/src/main/java/org/mybatis/dynamic/sql/select/aggregate/Avg.java index 1aaf3289c..0e04d78ea 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/aggregate/Avg.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/aggregate/Avg.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -16,24 +16,28 @@ package org.mybatis.dynamic.sql.select.aggregate; import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.select.function.AbstractUniTypeFunction; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; -public class Avg extends AbstractAggregate { +public class Avg extends AbstractUniTypeFunction> { private Avg(BasicColumn column) { super(column); } - + @Override - protected String render(String columnName) { - return "avg(" + columnName + ")"; //$NON-NLS-1$ //$NON-NLS-2$ + public FragmentAndParameters render(RenderingContext renderingContext) { + return column.render(renderingContext).mapFragment(s -> "avg(" + s + ")"); //$NON-NLS-1$ //$NON-NLS-2$ } @Override - protected Avg copy() { - return new Avg(column); + protected Avg copy() { + return new Avg<>(column); } - - public static Avg of(BasicColumn column) { - return new Avg(column); + + public static Avg of(BindableColumn column) { + return new Avg<>(column); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/aggregate/Count.java b/src/main/java/org/mybatis/dynamic/sql/select/aggregate/Count.java index 5578c46db..9d2a791cf 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/aggregate/Count.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/aggregate/Count.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,24 +15,35 @@ */ package org.mybatis.dynamic.sql.select.aggregate; +import java.util.Objects; + import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + +public class Count extends AbstractCount { + + private final BasicColumn column; -public class Count extends AbstractAggregate { - private Count(BasicColumn column) { - super(column); + this.column = Objects.requireNonNull(column); + } + + private Count(BasicColumn column, String alias) { + super(alias); + this.column = Objects.requireNonNull(column); } - + @Override - protected String render(String columnName) { - return "count(" + columnName + ")"; //$NON-NLS-1$ //$NON-NLS-2$ + public FragmentAndParameters render(RenderingContext renderingContext) { + return column.render(renderingContext).mapFragment(s -> "count(" + s + ")"); //$NON-NLS-1$ //$NON-NLS-2$ } @Override - protected Count copy() { - return new Count(column); + public Count as(String alias) { + return new Count(column, alias); } - + public static Count of(BasicColumn column) { return new Count(column); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/aggregate/CountAll.java b/src/main/java/org/mybatis/dynamic/sql/select/aggregate/CountAll.java index a4c911fd9..306c14f54 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/aggregate/CountAll.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/aggregate/CountAll.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,40 +15,26 @@ */ package org.mybatis.dynamic.sql.select.aggregate; -import java.util.Optional; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; -import org.mybatis.dynamic.sql.BasicColumn; -import org.mybatis.dynamic.sql.render.TableAliasCalculator; - -/** - * CountAll seems like the other aggregates, but it is special because there is no column. - * Rather than dealing with a useless and confusing abstraction, we simply implement - * BasicColumn directly. - * - * @author Jeff Butler - */ -public class CountAll implements BasicColumn { - - private String alias; +public class CountAll extends AbstractCount { public CountAll() { super(); } - @Override - public String renderWithTableAlias(TableAliasCalculator tableAliasCalculator) { - return "count(*)"; //$NON-NLS-1$ + private CountAll(String alias) { + super(alias); } @Override - public Optional alias() { - return Optional.ofNullable(alias); + public FragmentAndParameters render(RenderingContext renderingContext) { + return FragmentAndParameters.fromFragment("count(*)"); //$NON-NLS-1$ } @Override public CountAll as(String alias) { - CountAll copy = new CountAll(); - copy.alias = alias; - return copy; + return new CountAll(alias); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/aggregate/CountDistinct.java b/src/main/java/org/mybatis/dynamic/sql/select/aggregate/CountDistinct.java index 7435b4568..4acba9db9 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/aggregate/CountDistinct.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/aggregate/CountDistinct.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,24 +15,36 @@ */ package org.mybatis.dynamic.sql.select.aggregate; +import java.util.Objects; + import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + +public class CountDistinct extends AbstractCount { + + private final BasicColumn column; -public class CountDistinct extends AbstractAggregate { - private CountDistinct(BasicColumn column) { - super(column); + this.column = Objects.requireNonNull(column); + } + + private CountDistinct(BasicColumn column, String alias) { + super(alias); + this.column = Objects.requireNonNull(column); } - + @Override - protected String render(String columnName) { - return "count(distinct " + columnName + ")"; //$NON-NLS-1$ //$NON-NLS-2$ + public FragmentAndParameters render(RenderingContext renderingContext) { + return column.render(renderingContext) + .mapFragment(s -> "count(distinct " + s + ")"); //$NON-NLS-1$ //$NON-NLS-2$) } @Override - protected CountDistinct copy() { - return new CountDistinct(column); + public CountDistinct as(String alias) { + return new CountDistinct(column, alias); } - + public static CountDistinct of(BasicColumn column) { return new CountDistinct(column); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/aggregate/Max.java b/src/main/java/org/mybatis/dynamic/sql/select/aggregate/Max.java index a41294486..5d20594ea 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/aggregate/Max.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/aggregate/Max.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -16,24 +16,28 @@ package org.mybatis.dynamic.sql.select.aggregate; import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.select.function.AbstractUniTypeFunction; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; -public class Max extends AbstractAggregate { +public class Max extends AbstractUniTypeFunction> { private Max(BasicColumn column) { super(column); } - + @Override - protected String render(String columnName) { - return "max(" + columnName + ")"; //$NON-NLS-1$ //$NON-NLS-2$ + public FragmentAndParameters render(RenderingContext renderingContext) { + return column.render(renderingContext).mapFragment(s -> "max(" + s + ")"); //$NON-NLS-1$ //$NON-NLS-2$ } @Override - protected Max copy() { - return new Max(column); + protected Max copy() { + return new Max<>(column); } - - public static Max of(BasicColumn column) { - return new Max(column); + + public static Max of(BindableColumn column) { + return new Max<>(column); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/aggregate/Min.java b/src/main/java/org/mybatis/dynamic/sql/select/aggregate/Min.java index 09b57869b..e838c2f3a 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/aggregate/Min.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/aggregate/Min.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -16,24 +16,28 @@ package org.mybatis.dynamic.sql.select.aggregate; import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.select.function.AbstractUniTypeFunction; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; -public class Min extends AbstractAggregate { +public class Min extends AbstractUniTypeFunction> { private Min(BasicColumn column) { super(column); } - + @Override - protected String render(String columnName) { - return "min(" + columnName + ")"; //$NON-NLS-1$ //$NON-NLS-2$ + public FragmentAndParameters render(RenderingContext renderingContext) { + return column.render(renderingContext).mapFragment(s -> "min(" + s + ")"); //$NON-NLS-1$ //$NON-NLS-2$) } @Override - protected Min copy() { - return new Min(column); + protected Min copy() { + return new Min<>(column); } - - public static Min of(BasicColumn column) { - return new Min(column); + + public static Min of(BindableColumn column) { + return new Min<>(column); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/aggregate/Sum.java b/src/main/java/org/mybatis/dynamic/sql/select/aggregate/Sum.java index 33378c0bb..7ccfd7854 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/aggregate/Sum.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/aggregate/Sum.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,25 +15,68 @@ */ package org.mybatis.dynamic.sql.select.aggregate; +import java.util.function.Function; + import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.RenderableCondition; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.select.function.AbstractUniTypeFunction; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.Validator; +import org.mybatis.dynamic.sql.where.render.ColumnAndConditionRenderer; -public class Sum extends AbstractAggregate { +public class Sum extends AbstractUniTypeFunction> { + private final Function renderer; private Sum(BasicColumn column) { super(column); + renderer = rc -> column.render(rc).mapFragment(this::applyAggregate); + } + + private Sum(BindableColumn column, RenderableCondition condition) { + super(column); + renderer = rc -> { + Validator.assertTrue(condition.shouldRender(rc), "ERROR.37", "sum"); //$NON-NLS-1$ //$NON-NLS-2$ + + return new ColumnAndConditionRenderer.Builder() + .withColumn(column) + .withCondition(condition) + .withRenderingContext(rc) + .build() + .render() + .mapFragment(this::applyAggregate); + }; + } + + private Sum(BasicColumn column, Function renderer) { + super(column); + this.renderer = renderer; } - + @Override - protected String render(String columnName) { - return "sum(" + columnName + ")"; //$NON-NLS-1$ //$NON-NLS-2$ + public FragmentAndParameters render(RenderingContext renderingContext) { + return renderer.apply(renderingContext); + } + + private String applyAggregate(String s) { + return "sum(" + s + ")"; //$NON-NLS-1$ //$NON-NLS-2$ } @Override - protected Sum copy() { - return new Sum(column); + protected Sum copy() { + return new Sum<>(column, renderer); + } + + public static Sum of(BindableColumn column) { + return new Sum<>(column); + } + + public static Sum of(BasicColumn column) { + return new Sum<>(column); } - - public static Sum of(BasicColumn column) { - return new Sum(column); + + public static Sum of(BindableColumn column, RenderableCondition condition) { + return new Sum<>(column, condition); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/aggregate/package-info.java b/src/main/java/org/mybatis/dynamic/sql/select/aggregate/package-info.java new file mode 100644 index 000000000..60e9e8918 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/aggregate/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +@NullMarked +package org.mybatis.dynamic.sql.select.aggregate; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/BasicWhenCondition.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/BasicWhenCondition.java new file mode 100644 index 000000000..dcfaddc43 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/BasicWhenCondition.java @@ -0,0 +1,40 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select.caseexpression; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import org.mybatis.dynamic.sql.BasicColumn; + +public class BasicWhenCondition extends SimpleCaseWhenCondition { + private final List conditions = new ArrayList<>(); + + public BasicWhenCondition(List conditions, BasicColumn thenValue) { + super(thenValue); + this.conditions.addAll(conditions); + } + + public Stream conditions() { + return conditions.stream(); + } + + @Override + public R accept(SimpleCaseWhenConditionVisitor visitor) { + return visitor.visit(this); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ConditionBasedWhenCondition.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ConditionBasedWhenCondition.java new file mode 100644 index 000000000..78f841b1d --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ConditionBasedWhenCondition.java @@ -0,0 +1,41 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select.caseexpression; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.RenderableCondition; + +public class ConditionBasedWhenCondition extends SimpleCaseWhenCondition { + private final List> conditions = new ArrayList<>(); + + public ConditionBasedWhenCondition(List> conditions, BasicColumn thenValue) { + super(thenValue); + this.conditions.addAll(conditions); + } + + public Stream> conditions() { + return conditions.stream(); + } + + @Override + public R accept(SimpleCaseWhenConditionVisitor visitor) { + return visitor.visit(this); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ElseDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ElseDSL.java new file mode 100644 index 000000000..39d0bc740 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ElseDSL.java @@ -0,0 +1,51 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select.caseexpression; + +import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.Constant; +import org.mybatis.dynamic.sql.StringConstant; + +public interface ElseDSL { + + @SuppressWarnings("java:S100") + default T else_(String value) { + return else_(StringConstant.of(value)); + } + + @SuppressWarnings("java:S100") + default T else_(Boolean value) { + return else_(Constant.of(value.toString())); + } + + @SuppressWarnings("java:S100") + default T else_(Integer value) { + return else_(Constant.of(value.toString())); + } + + @SuppressWarnings("java:S100") + default T else_(Long value) { + return else_(Constant.of(value.toString())); + } + + @SuppressWarnings("java:S100") + default T else_(Double value) { + return else_(Constant.of(value.toString())); + } + + @SuppressWarnings("java:S100") + T else_(BasicColumn column); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseDSL.java new file mode 100644 index 000000000..c86bf7ff0 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseDSL.java @@ -0,0 +1,112 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select.caseexpression; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.AndOrCriteriaGroup; +import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.ColumnAndConditionCriterion; +import org.mybatis.dynamic.sql.CriteriaGroup; +import org.mybatis.dynamic.sql.RenderableCondition; +import org.mybatis.dynamic.sql.SqlCriterion; +import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionDSL; + +public class SearchedCaseDSL implements ElseDSL { + private final List whenConditions = new ArrayList<>(); + private @Nullable BasicColumn elseValue; + + public WhenDSL when(BindableColumn column, RenderableCondition condition, + AndOrCriteriaGroup... subCriteria) { + return when(column, condition, Arrays.asList(subCriteria)); + } + + public WhenDSL when(BindableColumn column, RenderableCondition condition, + List subCriteria) { + SqlCriterion sqlCriterion = ColumnAndConditionCriterion.withColumn(column) + .withCondition(condition) + .withSubCriteria(subCriteria) + .build(); + + return initialize(sqlCriterion); + } + + public WhenDSL when(SqlCriterion initialCriterion, AndOrCriteriaGroup... subCriteria) { + return when(initialCriterion, Arrays.asList(subCriteria)); + } + + public WhenDSL when(SqlCriterion initialCriterion, List subCriteria) { + SqlCriterion sqlCriterion = new CriteriaGroup.Builder() + .withInitialCriterion(initialCriterion) + .withSubCriteria(subCriteria) + .build(); + + return initialize(sqlCriterion); + } + + private WhenDSL initialize(SqlCriterion sqlCriterion) { + return new WhenDSL(sqlCriterion); + } + + @SuppressWarnings("java:S100") + @Override + public SearchedCaseEnder else_(BasicColumn column) { + elseValue = column; + return new SearchedCaseEnder(); + } + + public SearchedCaseModel end() { + return new SearchedCaseModel.Builder() + .withElseValue(elseValue) + .withWhenConditions(whenConditions) + .build(); + } + + public class WhenDSL extends AbstractBooleanExpressionDSL implements ThenDSL { + private WhenDSL(SqlCriterion sqlCriterion) { + setInitialCriterion(sqlCriterion); + } + + @Override + public SearchedCaseDSL then(BasicColumn column) { + whenConditions.add(new SearchedCaseWhenCondition.Builder() + .withInitialCriterion(getInitialCriterion()) + .withSubCriteria(subCriteria) + .withThenValue(column) + .build()); + return SearchedCaseDSL.this; + } + + @Override + protected WhenDSL getThis() { + return this; + } + } + + public class SearchedCaseEnder { + public SearchedCaseModel end() { + return SearchedCaseDSL.this.end(); + } + } + + public static SearchedCaseDSL searchedCase() { + return new SearchedCaseDSL(); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseModel.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseModel.java new file mode 100644 index 000000000..c42372868 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseModel.java @@ -0,0 +1,116 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select.caseexpression; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.SortSpecification; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.select.render.SearchedCaseRenderer; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.Validator; + +public class SearchedCaseModel implements BasicColumn, SortSpecification { + private final List whenConditions; + private final @Nullable BasicColumn elseValue; + private final @Nullable String alias; + private final String descendingPhrase; + + private SearchedCaseModel(Builder builder) { + whenConditions = builder.whenConditions; + alias = builder.alias; + elseValue = builder.elseValue; + descendingPhrase = builder.descendingPhrase; + Validator.assertNotEmpty(whenConditions, "ERROR.40"); //$NON-NLS-1$ + } + + public Stream whenConditions() { + return whenConditions.stream(); + } + + public Optional elseValue() { + return Optional.ofNullable(elseValue); + } + + @Override + public Optional alias() { + return Optional.ofNullable(alias); + } + + @Override + public SearchedCaseModel as(String alias) { + return new Builder().withWhenConditions(whenConditions) + .withElseValue(elseValue) + .withAlias(alias) + .withDescendingPhrase(descendingPhrase) + .build(); + } + + @Override + public SearchedCaseModel descending() { + return new Builder().withWhenConditions(whenConditions) + .withElseValue(elseValue) + .withAlias(alias) + .withDescendingPhrase(" DESC") //$NON-NLS-1$ + .build(); + } + + @Override + public FragmentAndParameters renderForOrderBy(RenderingContext renderingContext) { + return render(renderingContext).mapFragment(f -> f + descendingPhrase); + } + + @Override + public FragmentAndParameters render(RenderingContext renderingContext) { + return new SearchedCaseRenderer(this, renderingContext).render(); + } + + public static class Builder { + private final List whenConditions = new ArrayList<>(); + private @Nullable BasicColumn elseValue; + private @Nullable String alias; + private String descendingPhrase = ""; //$NON-NLS-1$ + + public Builder withWhenConditions(List whenConditions) { + this.whenConditions.addAll(whenConditions); + return this; + } + + public Builder withElseValue(@Nullable BasicColumn elseValue) { + this.elseValue = elseValue; + return this; + } + + public Builder withAlias(@Nullable String alias) { + this.alias = alias; + return this; + } + + public Builder withDescendingPhrase(String descendingPhrase) { + this.descendingPhrase = descendingPhrase; + return this; + } + + public SearchedCaseModel build() { + return new SearchedCaseModel(this); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseWhenCondition.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseWhenCondition.java new file mode 100644 index 000000000..9b251be9c --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseWhenCondition.java @@ -0,0 +1,53 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select.caseexpression; + +import java.util.Objects; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionModel; + +public class SearchedCaseWhenCondition extends AbstractBooleanExpressionModel { + private final BasicColumn thenValue; + + public BasicColumn thenValue() { + return thenValue; + } + + private SearchedCaseWhenCondition(Builder builder) { + super(builder); + thenValue = Objects.requireNonNull(builder.thenValue); + } + + public static class Builder extends AbstractBuilder { + private @Nullable BasicColumn thenValue; + + public Builder withThenValue(BasicColumn thenValue) { + this.thenValue = thenValue; + return this; + } + + public SearchedCaseWhenCondition build() { + return new SearchedCaseWhenCondition(this); + } + + @Override + protected Builder getThis() { + return this; + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseDSL.java new file mode 100644 index 000000000..be2e1e908 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseDSL.java @@ -0,0 +1,112 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select.caseexpression; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.RenderableCondition; + +public class SimpleCaseDSL implements ElseDSL.SimpleCaseEnder> { + private final BindableColumn column; + private final List> whenConditions = new ArrayList<>(); + private @Nullable BasicColumn elseValue; + + private SimpleCaseDSL(BindableColumn column) { + this.column = Objects.requireNonNull(column); + } + + @SafeVarargs + public final ConditionBasedWhenFinisher when(RenderableCondition condition, + RenderableCondition... subsequentConditions) { + return when(condition, Arrays.asList(subsequentConditions)); + } + + public ConditionBasedWhenFinisher when(RenderableCondition condition, + List> subsequentConditions) { + return new ConditionBasedWhenFinisher(condition, subsequentConditions); + } + + @SafeVarargs + public final BasicWhenFinisher when(T condition, T... subsequentConditions) { + return when(condition, Arrays.asList(subsequentConditions)); + } + + public BasicWhenFinisher when(T condition, List subsequentConditions) { + return new BasicWhenFinisher(condition, subsequentConditions); + } + + @SuppressWarnings("java:S100") + @Override + public SimpleCaseEnder else_(BasicColumn column) { + elseValue = column; + return new SimpleCaseEnder(); + } + + public SimpleCaseModel end() { + return new SimpleCaseModel.Builder() + .withColumn(column) + .withWhenConditions(whenConditions) + .withElseValue(elseValue) + .build(); + } + + public class ConditionBasedWhenFinisher implements ThenDSL> { + private final List> conditions = new ArrayList<>(); + + private ConditionBasedWhenFinisher(RenderableCondition condition, + List> subsequentConditions) { + conditions.add(condition); + conditions.addAll(subsequentConditions); + } + + @Override + public SimpleCaseDSL then(BasicColumn column) { + whenConditions.add(new ConditionBasedWhenCondition<>(conditions, column)); + return SimpleCaseDSL.this; + } + } + + public class BasicWhenFinisher implements ThenDSL> { + private final List values = new ArrayList<>(); + + private BasicWhenFinisher(T value, List subsequentValues) { + values.add(value); + values.addAll(subsequentValues); + } + + @Override + public SimpleCaseDSL then(BasicColumn column) { + whenConditions.add(new BasicWhenCondition<>(values, column)); + return SimpleCaseDSL.this; + } + } + + public class SimpleCaseEnder { + public SimpleCaseModel end() { + return SimpleCaseDSL.this.end(); + } + } + + public static SimpleCaseDSL simpleCase(BindableColumn column) { + return new SimpleCaseDSL<>(column); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseModel.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseModel.java new file mode 100644 index 000000000..45eb95c01 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseModel.java @@ -0,0 +1,134 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select.caseexpression; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.SortSpecification; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.select.render.SimpleCaseRenderer; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.Validator; + +public class SimpleCaseModel implements BasicColumn, SortSpecification { + private final BindableColumn column; + private final List> whenConditions; + private final @Nullable BasicColumn elseValue; + private final @Nullable String alias; + private final String descendingPhrase; + + private SimpleCaseModel(Builder builder) { + column = Objects.requireNonNull(builder.column); + whenConditions = builder.whenConditions; + elseValue = builder.elseValue; + alias = builder.alias; + descendingPhrase = builder.descendingPhrase; + Validator.assertNotEmpty(whenConditions, "ERROR.40"); //$NON-NLS-1$ + } + + public BindableColumn column() { + return column; + } + + public Stream> whenConditions() { + return whenConditions.stream(); + } + + public Optional elseValue() { + return Optional.ofNullable(elseValue); + } + + @Override + public Optional alias() { + return Optional.ofNullable(alias); + } + + @Override + public SimpleCaseModel as(String alias) { + return new Builder() + .withColumn(column) + .withWhenConditions(whenConditions) + .withElseValue(elseValue) + .withAlias(alias) + .withDescendingPhrase(descendingPhrase) + .build(); + } + + @Override + public SimpleCaseModel descending() { + return new Builder() + .withColumn(column) + .withWhenConditions(whenConditions) + .withElseValue(elseValue) + .withAlias(alias) + .withDescendingPhrase(" DESC") //$NON-NLS-1$ + .build(); + } + + @Override + public FragmentAndParameters renderForOrderBy(RenderingContext renderingContext) { + return render(renderingContext).mapFragment(f -> f + descendingPhrase); + } + + @Override + public FragmentAndParameters render(RenderingContext renderingContext) { + return new SimpleCaseRenderer<>(this, renderingContext).render(); + } + + public static class Builder { + private @Nullable BindableColumn column; + private final List> whenConditions = new ArrayList<>(); + private @Nullable BasicColumn elseValue; + private @Nullable String alias; + private String descendingPhrase = ""; //$NON-NLS-1$ + + public Builder withColumn(BindableColumn column) { + this.column = column; + return this; + } + + public Builder withWhenConditions(List> whenConditions) { + this.whenConditions.addAll(whenConditions); + return this; + } + + public Builder withElseValue(@Nullable BasicColumn elseValue) { + this.elseValue = elseValue; + return this; + } + + public Builder withAlias(@Nullable String alias) { + this.alias = alias; + return this; + } + + public Builder withDescendingPhrase(String descendingPhrase) { + this.descendingPhrase = descendingPhrase; + return this; + } + + public SimpleCaseModel build() { + return new SimpleCaseModel<>(this); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseWhenCondition.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseWhenCondition.java new file mode 100644 index 000000000..7f6351e02 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseWhenCondition.java @@ -0,0 +1,34 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select.caseexpression; + +import java.util.Objects; + +import org.mybatis.dynamic.sql.BasicColumn; + +public abstract class SimpleCaseWhenCondition { + private final BasicColumn thenValue; + + protected SimpleCaseWhenCondition(BasicColumn thenValue) { + this.thenValue = Objects.requireNonNull(thenValue); + } + + public BasicColumn thenValue() { + return thenValue; + } + + public abstract R accept(SimpleCaseWhenConditionVisitor visitor); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseWhenConditionVisitor.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseWhenConditionVisitor.java new file mode 100644 index 000000000..dadef7455 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseWhenConditionVisitor.java @@ -0,0 +1,22 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select.caseexpression; + +public interface SimpleCaseWhenConditionVisitor { + R visit(ConditionBasedWhenCondition whenCondition); + + R visit(BasicWhenCondition whenCondition); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ThenDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ThenDSL.java new file mode 100644 index 000000000..29c914731 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ThenDSL.java @@ -0,0 +1,45 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select.caseexpression; + +import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.Constant; +import org.mybatis.dynamic.sql.StringConstant; + +public interface ThenDSL { + + default T then(String value) { + return then(StringConstant.of(value)); + } + + default T then(Boolean value) { + return then(Constant.of(value.toString())); + } + + default T then(Integer value) { + return then(Constant.of(value.toString())); + } + + default T then(Long value) { + return then(Constant.of(value.toString())); + } + + default T then(Double value) { + return then(Constant.of(value.toString())); + } + + T then(BasicColumn column); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/package-info.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/package-info.java new file mode 100644 index 000000000..a383bc305 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +@NullMarked +package org.mybatis.dynamic.sql.select.caseexpression; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/mybatis/dynamic/sql/select/function/AbstractMultipleColumnArithmeticFunction.java b/src/main/java/org/mybatis/dynamic/sql/select/function/AbstractMultipleColumnArithmeticFunction.java deleted file mode 100644 index cc387ead7..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/select/function/AbstractMultipleColumnArithmeticFunction.java +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright 2016-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.select.function; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.mybatis.dynamic.sql.BasicColumn; -import org.mybatis.dynamic.sql.BindableColumn; -import org.mybatis.dynamic.sql.render.TableAliasCalculator; - -public abstract class AbstractMultipleColumnArithmeticFunction> - extends AbstractFunction> { - - protected BasicColumn secondColumn; - protected List subsequentColumns = new ArrayList<>(); - - protected AbstractMultipleColumnArithmeticFunction(BindableColumn firstColumn, BasicColumn secondColumn, - List subsequentColumns) { - super(firstColumn); - this.secondColumn = Objects.requireNonNull(secondColumn); - this.subsequentColumns.addAll(subsequentColumns); - } - - @Override - public String renderWithTableAlias(TableAliasCalculator tableAliasCalculator) { - // note - the cast below is added for a type inference bug in the Java9 compiler. - return Stream.of(Stream.of((BasicColumn) column), Stream.of(secondColumn), subsequentColumns.stream()) - .flatMap(Function.identity()) - .map(column -> column.renderWithTableAlias(tableAliasCalculator)) - .collect(Collectors.joining(padOperator(), "(", ")")); //$NON-NLS-1$ //$NON-NLS-2$ - } - - private String padOperator() { - return " " + operator() + " "; //$NON-NLS-1$ //$NON-NLS-2$ - } - - protected abstract String operator(); -} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/function/AbstractTypeConvertingFunction.java b/src/main/java/org/mybatis/dynamic/sql/select/function/AbstractTypeConvertingFunction.java new file mode 100644 index 000000000..1517d3519 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/function/AbstractTypeConvertingFunction.java @@ -0,0 +1,63 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select.function; + +import java.util.Objects; +import java.util.Optional; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.BindableColumn; + +/** + * Represents a function that can change the underlying type. For example, converting a binary field for a base64 + * string, or an integer to a string, etc. + * + *

Thanks to @endink for the idea. + * + * @author Jeff Butler + * + * @param The type of the underlying column. For example, if a function converts a VARCHAR to an INT, then the + * underlying type will be a String + * @param The type of the column after the conversion. For example, if a function converts a VARCHAR to an INT, then + * the converted type will be Integer + * @param the specific subtype that implements the function + */ +public abstract class AbstractTypeConvertingFunction> + implements BindableColumn { + protected final BasicColumn column; + protected @Nullable String alias; + + protected AbstractTypeConvertingFunction(BasicColumn column) { + this.column = Objects.requireNonNull(column); + } + + @Override + public Optional alias() { + return Optional.ofNullable(alias); + } + + @NonNull + @Override + public U as(String alias) { + U newThing = copy(); + newThing.alias = alias; + return newThing; + } + + protected abstract U copy(); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/function/AbstractFunction.java b/src/main/java/org/mybatis/dynamic/sql/select/function/AbstractUniTypeFunction.java similarity index 53% rename from src/main/java/org/mybatis/dynamic/sql/select/function/AbstractFunction.java rename to src/main/java/org/mybatis/dynamic/sql/select/function/AbstractUniTypeFunction.java index 5604af282..0ad9ee576 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/function/AbstractFunction.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/function/AbstractUniTypeFunction.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -16,29 +16,25 @@ package org.mybatis.dynamic.sql.select.function; import java.sql.JDBCType; -import java.util.Objects; import java.util.Optional; -import org.mybatis.dynamic.sql.BindableColumn; - -public abstract class AbstractFunction> implements BindableColumn { - protected BindableColumn column; - protected String alias; +import org.mybatis.dynamic.sql.BasicColumn; - protected AbstractFunction(BindableColumn column) { - this.column = Objects.requireNonNull(column); - } - - @Override - public Optional alias() { - return Optional.ofNullable(alias); - } +/** + * Represents a function that does not change the underlying data type. + * + * @author Jeff Butler + * + * @param + * The type of the underlying column + * @param + * the specific subtype that implements the function + */ +public abstract class AbstractUniTypeFunction> + extends AbstractTypeConvertingFunction { - @Override - public U as(String alias) { - U newThing = copy(); - newThing.alias = alias; - return newThing; + protected AbstractUniTypeFunction(BasicColumn column) { + super(column); } @Override @@ -50,6 +46,4 @@ public Optional jdbcType() { public Optional typeHandler() { return column.typeHandler(); } - - protected abstract U copy(); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/function/Add.java b/src/main/java/org/mybatis/dynamic/sql/select/function/Add.java index 72100276a..7521b9fd3 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/function/Add.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/function/Add.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,16 +15,17 @@ */ package org.mybatis.dynamic.sql.select.function; +import java.util.Arrays; import java.util.List; import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.BindableColumn; -public class Add extends AbstractMultipleColumnArithmeticFunction> { - - private Add(BindableColumn firstColumn, BasicColumn secondColumn, +public class Add extends OperatorFunction { + + private Add(BasicColumn firstColumn, BasicColumn secondColumn, List subsequentColumns) { - super(firstColumn, secondColumn, subsequentColumns); + super("+", firstColumn, secondColumn, subsequentColumns); //$NON-NLS-1$ } @Override @@ -32,12 +33,12 @@ protected Add copy() { return new Add<>(column, secondColumn, subsequentColumns); } - @Override - protected String operator() { - return "+"; //$NON-NLS-1$ + public static Add of(BindableColumn firstColumn, BasicColumn secondColumn, + BasicColumn... subsequentColumns) { + return of(firstColumn, secondColumn, Arrays.asList(subsequentColumns)); } - public static Add of(BindableColumn firstColumn, BasicColumn secondColumn, + public static Add of(BindableColumn firstColumn, BasicColumn secondColumn, List subsequentColumns) { return new Add<>(firstColumn, secondColumn, subsequentColumns); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/function/Cast.java b/src/main/java/org/mybatis/dynamic/sql/select/function/Cast.java new file mode 100644 index 000000000..1349d4f51 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/function/Cast.java @@ -0,0 +1,83 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select.function; + +import java.util.Objects; +import java.util.Optional; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + +public class Cast implements BasicColumn { + private final BasicColumn column; + private final String targetType; + private final @Nullable String alias; + + private Cast(Builder builder) { + column = Objects.requireNonNull(builder.column); + targetType = Objects.requireNonNull(builder.targetType); + alias = builder.alias; + } + + @Override + public Optional alias() { + return Optional.ofNullable(alias); + } + + @Override + public Cast as(String alias) { + return new Builder().withColumn(column) + .withTargetType(targetType) + .withAlias(alias) + .build(); + } + + @Override + public FragmentAndParameters render(RenderingContext renderingContext) { + return column.render(renderingContext).mapFragment(this::applyCast); + } + + private String applyCast(String in) { + return "cast(" + in + " as " + targetType + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + + public static class Builder { + private @Nullable BasicColumn column; + private @Nullable String targetType; + private @Nullable String alias; + + public Builder withColumn(BasicColumn column) { + this.column = column; + return this; + } + + public Builder withTargetType(String targetType) { + this.targetType = targetType; + return this; + } + + public Builder withAlias(String alias) { + this.alias = alias; + return this; + } + + public Cast build() { + return new Cast(this); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/function/Concat.java b/src/main/java/org/mybatis/dynamic/sql/select/function/Concat.java new file mode 100644 index 000000000..2dbf84b7b --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/function/Concat.java @@ -0,0 +1,59 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select.function; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.FragmentCollector; + +public class Concat extends AbstractUniTypeFunction> { + private final List allColumns = new ArrayList<>(); + + protected Concat(BasicColumn firstColumn, List subsequentColumns) { + super(firstColumn); + allColumns.add(firstColumn); + this.allColumns.addAll(subsequentColumns); + } + + @Override + public FragmentAndParameters render(RenderingContext renderingContext) { + // note - the cast below is added for type inference issues in some compilers + return allColumns.stream() + .map(column -> column.render(renderingContext)) + .collect(FragmentCollector.collect()).toFragmentAndParameters( + Collectors.joining(", ", "concat(", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + + @Override + protected Concat copy() { + return new Concat<>(column, allColumns.subList(1, allColumns.size())); + } + + public static Concat concat(BindableColumn firstColumn, BasicColumn... subsequentColumns) { + return new Concat<>(firstColumn, Arrays.asList(subsequentColumns)); + } + + public static Concat of(BindableColumn firstColumn, List subsequentColumns) { + return new Concat<>(firstColumn, subsequentColumns); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/function/Concatenate.java b/src/main/java/org/mybatis/dynamic/sql/select/function/Concatenate.java new file mode 100644 index 000000000..16357b21f --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/function/Concatenate.java @@ -0,0 +1,45 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select.function; + +import java.util.Arrays; +import java.util.List; + +import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.BindableColumn; + +public class Concatenate extends OperatorFunction { + + protected Concatenate(BasicColumn firstColumn, BasicColumn secondColumn, + List subsequentColumns) { + super("||", firstColumn, secondColumn, subsequentColumns); //$NON-NLS-1$ + } + + @Override + protected Concatenate copy() { + return new Concatenate<>(column, secondColumn, subsequentColumns); + } + + public static Concatenate concatenate(BindableColumn firstColumn, BasicColumn secondColumn, + BasicColumn... subsequentColumns) { + return new Concatenate<>(firstColumn, secondColumn, Arrays.asList(subsequentColumns)); + } + + public static Concatenate of(BindableColumn firstColumn, BasicColumn secondColumn, + List subsequentColumns) { + return new Concatenate<>(firstColumn, secondColumn, subsequentColumns); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/function/Divide.java b/src/main/java/org/mybatis/dynamic/sql/select/function/Divide.java index 1489afc1b..0463798b5 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/function/Divide.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/function/Divide.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,16 +15,17 @@ */ package org.mybatis.dynamic.sql.select.function; +import java.util.Arrays; import java.util.List; import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.BindableColumn; -public class Divide extends AbstractMultipleColumnArithmeticFunction> { - - private Divide(BindableColumn firstColumn, BasicColumn secondColumn, +public class Divide extends OperatorFunction { + + private Divide(BasicColumn firstColumn, BasicColumn secondColumn, List subsequentColumns) { - super(firstColumn, secondColumn, subsequentColumns); + super("/", firstColumn, secondColumn, subsequentColumns); //$NON-NLS-1$ } @Override @@ -32,12 +33,12 @@ protected Divide copy() { return new Divide<>(column, secondColumn, subsequentColumns); } - @Override - protected String operator() { - return "/"; //$NON-NLS-1$ + public static Divide of(BindableColumn firstColumn, BasicColumn secondColumn, + BasicColumn... subsequentColumns) { + return of(firstColumn, secondColumn, Arrays.asList(subsequentColumns)); } - public static Divide of(BindableColumn firstColumn, BasicColumn secondColumn, + public static Divide of(BindableColumn firstColumn, BasicColumn secondColumn, List subsequentColumns) { return new Divide<>(firstColumn, secondColumn, subsequentColumns); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/function/Lower.java b/src/main/java/org/mybatis/dynamic/sql/select/function/Lower.java index 184d4e8dc..80cd292d9 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/function/Lower.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/function/Lower.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,28 +15,28 @@ */ package org.mybatis.dynamic.sql.select.function; +import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.BindableColumn; -import org.mybatis.dynamic.sql.render.TableAliasCalculator; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; -public class Lower extends AbstractFunction { - - private Lower(BindableColumn column) { +public class Lower extends AbstractUniTypeFunction> { + + private Lower(BasicColumn column) { super(column); } - + @Override - public String renderWithTableAlias(TableAliasCalculator tableAliasCalculator) { - return "lower(" //$NON-NLS-1$ - + column.renderWithTableAlias(tableAliasCalculator) - + ")"; //$NON-NLS-1$ + public FragmentAndParameters render(RenderingContext renderingContext) { + return column.render(renderingContext).mapFragment(s -> "lower(" + s + ")"); //$NON-NLS-1$ //$NON-NLS-2$ } @Override - protected Lower copy() { - return new Lower(column); + protected Lower copy() { + return new Lower<>(column); } - public static Lower of(BindableColumn column) { - return new Lower(column); + public static Lower of(BindableColumn column) { + return new Lower<>(column); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/function/Multiply.java b/src/main/java/org/mybatis/dynamic/sql/select/function/Multiply.java index 4fdd3f5d9..239e0564b 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/function/Multiply.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/function/Multiply.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,16 +15,17 @@ */ package org.mybatis.dynamic.sql.select.function; +import java.util.Arrays; import java.util.List; import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.BindableColumn; -public class Multiply extends AbstractMultipleColumnArithmeticFunction> { - - private Multiply(BindableColumn firstColumn, BasicColumn secondColumn, +public class Multiply extends OperatorFunction { + + private Multiply(BasicColumn firstColumn, BasicColumn secondColumn, List subsequentColumns) { - super(firstColumn, secondColumn, subsequentColumns); + super("*", firstColumn, secondColumn, subsequentColumns); //$NON-NLS-1$ } @Override @@ -32,12 +33,12 @@ protected Multiply copy() { return new Multiply<>(column, secondColumn, subsequentColumns); } - @Override - protected String operator() { - return "*"; //$NON-NLS-1$ + public static Multiply of(BindableColumn firstColumn, BasicColumn secondColumn, + BasicColumn... subsequentColumns) { + return of(firstColumn, secondColumn, Arrays.asList(subsequentColumns)); } - public static Multiply of(BindableColumn firstColumn, BasicColumn secondColumn, + public static Multiply of(BindableColumn firstColumn, BasicColumn secondColumn, List subsequentColumns) { return new Multiply<>(firstColumn, secondColumn, subsequentColumns); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/function/OperatorFunction.java b/src/main/java/org/mybatis/dynamic/sql/select/function/OperatorFunction.java new file mode 100644 index 000000000..154a5f564 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/function/OperatorFunction.java @@ -0,0 +1,71 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select.function; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.FragmentCollector; + +public class OperatorFunction extends AbstractUniTypeFunction> { + + protected final BasicColumn secondColumn; + protected final List subsequentColumns = new ArrayList<>(); + private final String operator; + + protected OperatorFunction(String operator, BasicColumn firstColumn, BasicColumn secondColumn, + List subsequentColumns) { + super(firstColumn); + this.secondColumn = Objects.requireNonNull(secondColumn); + this.subsequentColumns.addAll(subsequentColumns); + this.operator = Objects.requireNonNull(operator); + } + + @Override + protected OperatorFunction copy() { + return new OperatorFunction<>(operator, column, secondColumn, subsequentColumns); + } + + @Override + public FragmentAndParameters render(RenderingContext renderingContext) { + String paddedOperator = " " + operator + " "; //$NON-NLS-1$ //$NON-NLS-2$ + + return Stream.of(Stream.of(column, secondColumn), subsequentColumns.stream()) + .flatMap(Function.identity()) + .map(column -> column.render(renderingContext)) + .collect(FragmentCollector.collect()) + .toFragmentAndParameters(Collectors.joining(paddedOperator, "(", ")")); //$NON-NLS-1$ //$NON-NLS-2$ + } + + public static OperatorFunction of(String operator, BindableColumn firstColumn, BasicColumn secondColumn, + BasicColumn... subsequentColumns) { + return of(operator, firstColumn, secondColumn, Arrays.asList(subsequentColumns)); + } + + public static OperatorFunction of(String operator, BindableColumn firstColumn, BasicColumn secondColumn, + List subsequentColumns) { + return new OperatorFunction<>(operator, firstColumn, secondColumn, subsequentColumns); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/function/Substring.java b/src/main/java/org/mybatis/dynamic/sql/select/function/Substring.java index 9efb9a148..a987a3a1f 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/function/Substring.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/function/Substring.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,37 +15,39 @@ */ package org.mybatis.dynamic.sql.select.function; +import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.BindableColumn; -import org.mybatis.dynamic.sql.render.TableAliasCalculator; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; -public class Substring extends AbstractFunction { +public class Substring extends AbstractUniTypeFunction> { - private int offset; - private int length; - - private Substring(BindableColumn column, int offset, int length) { + private final int offset; + private final int length; + + private Substring(BasicColumn column, int offset, int length) { super(column); this.offset = offset; this.length = length; } - + @Override - public String renderWithTableAlias(TableAliasCalculator tableAliasCalculator) { - return "substring(" //$NON-NLS-1$ - + column.renderWithTableAlias(tableAliasCalculator) + public FragmentAndParameters render(RenderingContext renderingContext) { + return column.render(renderingContext).mapFragment(s -> "substring(" //$NON-NLS-1$ + + s + ", " //$NON-NLS-1$ + offset + ", " //$NON-NLS-1$ + length - + ")"; //$NON-NLS-1$ + + ")"); //$NON-NLS-1$ } - + @Override - protected Substring copy() { - return new Substring(column, offset, length); + protected Substring copy() { + return new Substring<>(column, offset, length); } - - public static Substring of(BindableColumn column, int offset, int length) { - return new Substring(column, offset, length); + + public static Substring of(BindableColumn column, int offset, int length) { + return new Substring<>(column, offset, length); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/function/Subtract.java b/src/main/java/org/mybatis/dynamic/sql/select/function/Subtract.java index d6b0482ab..ae128fc98 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/function/Subtract.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/function/Subtract.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,16 +15,17 @@ */ package org.mybatis.dynamic.sql.select.function; +import java.util.Arrays; import java.util.List; import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.BindableColumn; -public class Subtract extends AbstractMultipleColumnArithmeticFunction> { - - private Subtract(BindableColumn firstColumn, BasicColumn secondColumn, +public class Subtract extends OperatorFunction { + + private Subtract(BasicColumn firstColumn, BasicColumn secondColumn, List subsequentColumns) { - super(firstColumn, secondColumn, subsequentColumns); + super("-", firstColumn, secondColumn, subsequentColumns); //$NON-NLS-1$ } @Override @@ -32,13 +33,13 @@ protected Subtract copy() { return new Subtract<>(column, secondColumn, subsequentColumns); } - @Override - protected String operator() { - return "-"; //$NON-NLS-1$ + public static Subtract of(BindableColumn firstColumn, BasicColumn secondColumn, + BasicColumn... subsequentColumns) { + return of(firstColumn, secondColumn, Arrays.asList(subsequentColumns)); } - public static Subtract of(BindableColumn firstColumn, BasicColumn secondColumn, - List subsequentColumns) { + public static Subtract of(BindableColumn firstColumn, BasicColumn secondColumn, + List subsequentColumns) { return new Subtract<>(firstColumn, secondColumn, subsequentColumns); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/function/Upper.java b/src/main/java/org/mybatis/dynamic/sql/select/function/Upper.java index 8895b7e90..d4be9ff61 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/function/Upper.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/function/Upper.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,28 +15,28 @@ */ package org.mybatis.dynamic.sql.select.function; +import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.BindableColumn; -import org.mybatis.dynamic.sql.render.TableAliasCalculator; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; -public class Upper extends AbstractFunction { - - private Upper(BindableColumn column) { +public class Upper extends AbstractUniTypeFunction> { + + private Upper(BasicColumn column) { super(column); } @Override - public String renderWithTableAlias(TableAliasCalculator tableAliasCalculator) { - return "upper(" //$NON-NLS-1$ - + column.renderWithTableAlias(tableAliasCalculator) - + ")"; //$NON-NLS-1$ + public FragmentAndParameters render(RenderingContext renderingContext) { + return column.render(renderingContext).mapFragment(s -> "upper(" + s + ")"); //$NON-NLS-1$ //$NON-NLS-2$ } @Override - protected Upper copy() { - return new Upper(column); + protected Upper copy() { + return new Upper<>(column); } - public static Upper of(BindableColumn column) { - return new Upper(column); + public static Upper of(BindableColumn column) { + return new Upper<>(column); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/function/package-info.java b/src/main/java/org/mybatis/dynamic/sql/select/function/package-info.java new file mode 100644 index 000000000..4f8535279 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/function/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +@NullMarked +package org.mybatis.dynamic.sql.select.function; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinCriterion.java b/src/main/java/org/mybatis/dynamic/sql/select/join/JoinCriterion.java deleted file mode 100644 index 33983d691..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinCriterion.java +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright 2016-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.select.join; - -import java.util.Objects; - -import org.mybatis.dynamic.sql.BasicColumn; - -public class JoinCriterion { - - private String connector; - private BasicColumn leftColumn; - private JoinCondition joinCondition; - - private JoinCriterion(Builder builder) { - connector = Objects.requireNonNull(builder.connector); - leftColumn = Objects.requireNonNull(builder.joinColumn); - joinCondition = Objects.requireNonNull(builder.joinCondition); - } - - public String connector() { - return connector; - } - - public BasicColumn leftColumn() { - return leftColumn; - } - - public BasicColumn rightColumn() { - return joinCondition.rightColumn(); - } - - public String operator() { - return joinCondition.operator(); - } - - public static class Builder { - private String connector; - private BasicColumn joinColumn; - private JoinCondition joinCondition; - - public Builder withConnector(String connector) { - this.connector = connector; - return this; - } - - public Builder withJoinColumn(BasicColumn joinColumn) { - this.joinColumn = joinColumn; - return this; - } - - public Builder withJoinCondition(JoinCondition joinCondition) { - this.joinCondition = joinCondition; - return this; - } - - public JoinCriterion build() { - return new JoinCriterion(this); - } - } -} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinModel.java b/src/main/java/org/mybatis/dynamic/sql/select/join/JoinModel.java index 19f8b9938..fe987c1b4 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/join/JoinModel.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -17,21 +17,33 @@ import java.util.ArrayList; import java.util.List; -import java.util.function.Function; +import java.util.Objects; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.TableExpression; +import org.mybatis.dynamic.sql.util.Validator; + public class JoinModel { - private List joinSpecifications = new ArrayList<>(); - - private JoinModel(List joinSpecifications) { + private final List joinSpecifications = new ArrayList<>(); + + private JoinModel(@Nullable List joinSpecifications) { + Objects.requireNonNull(joinSpecifications); + Validator.assertNotEmpty(joinSpecifications, "ERROR.15"); //$NON-NLS-1$ this.joinSpecifications.addAll(joinSpecifications); } - public Stream mapJoinSpecifications(Function mapper) { - return joinSpecifications.stream().map(mapper); + public Stream joinSpecifications() { + return joinSpecifications.stream(); } - - public static JoinModel of(List joinSpecifications) { + + public static JoinModel of(@Nullable List joinSpecifications) { return new JoinModel(joinSpecifications); } + + public boolean containsSubQueries() { + return joinSpecifications.stream() + .map(JoinSpecification::table) + .anyMatch(TableExpression::isSubQuery); + } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinSpecification.java b/src/main/java/org/mybatis/dynamic/sql/select/join/JoinSpecification.java index 0482ef208..8cefbba6b 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinSpecification.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/join/JoinSpecification.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,69 +15,59 @@ */ package org.mybatis.dynamic.sql.select.join; -import java.util.ArrayList; -import java.util.List; import java.util.Objects; -import java.util.function.Function; -import java.util.stream.Stream; -import org.mybatis.dynamic.sql.SqlTable; +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.TableExpression; +import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionModel; +import org.mybatis.dynamic.sql.util.Validator; -public class JoinSpecification { +public class JoinSpecification extends AbstractBooleanExpressionModel { + + private final TableExpression table; + private final JoinType joinType; - private SqlTable table; - private List joinCriteria; - private JoinType joinType; - private JoinSpecification(Builder builder) { + super(builder); table = Objects.requireNonNull(builder.table); - joinCriteria = Objects.requireNonNull(builder.joinCriteria); joinType = Objects.requireNonNull(builder.joinType); + Validator.assertFalse(initialCriterion().isEmpty() && subCriteria().isEmpty(), + "ERROR.16"); //$NON-NLS-1$ } - - public SqlTable table() { + + public TableExpression table() { return table; } - - public Stream mapJoinCriteria(Function mapper) { - return joinCriteria.stream().map(mapper); - } - + public JoinType joinType() { return joinType; } - - public static Builder withJoinTable(SqlTable table) { + + public static Builder withJoinTable(TableExpression table) { return new Builder().withJoinTable(table); } - - public static class Builder { - private SqlTable table; - private List joinCriteria = new ArrayList<>(); - private JoinType joinType; - - public Builder withJoinTable(SqlTable table) { + + public static class Builder extends AbstractBuilder { + private @Nullable TableExpression table; + private @Nullable JoinType joinType; + + public Builder withJoinTable(TableExpression table) { this.table = table; return this; } - - public Builder withJoinCriterion(JoinCriterion joinCriterion) { - this.joinCriteria.add(joinCriterion); - return this; - } - - public Builder withJoinCriteria(List joinCriteria) { - this.joinCriteria.addAll(joinCriteria); - return this; - } - + public Builder withJoinType(JoinType joinType) { this.joinType = joinType; return this; } - + public JoinSpecification build() { return new JoinSpecification(this); } + + @Override + protected Builder getThis() { + return this; + } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinType.java b/src/main/java/org/mybatis/dynamic/sql/select/join/JoinType.java index a241269b7..e797d1d1d 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinType.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/join/JoinType.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,24 +15,19 @@ */ package org.mybatis.dynamic.sql.select.join; -import java.util.Optional; - public enum JoinType { - INNER(), - LEFT("left"), //$NON-NLS-1$ - RIGHT("right"), //$NON-NLS-1$ - FULL("full"); //$NON-NLS-1$ - - private String shortType; - - JoinType() { - } - - JoinType(String shortType) { - this.shortType = shortType; + INNER("join"), //$NON-NLS-1$ + LEFT("left join"), //$NON-NLS-1$ + RIGHT("right join"), //$NON-NLS-1$ + FULL("full join"); //$NON-NLS-1$ + + private final String type; + + JoinType(String type) { + this.type = type; } - - public Optional shortType() { - return Optional.ofNullable(shortType); + + public String type() { + return type; } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/EqualTo.java b/src/main/java/org/mybatis/dynamic/sql/select/join/package-info.java similarity index 60% rename from src/main/java/org/mybatis/dynamic/sql/select/join/EqualTo.java rename to src/main/java/org/mybatis/dynamic/sql/select/join/package-info.java index 76142a6e8..03ec51bcc 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/join/EqualTo.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/join/package-info.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -13,18 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@NullMarked package org.mybatis.dynamic.sql.select.join; -import org.mybatis.dynamic.sql.BasicColumn; - -public class EqualTo extends JoinCondition { - - public EqualTo(BasicColumn rightColumn) { - super(rightColumn); - } - - @Override - public String operator() { - return "="; //$NON-NLS-1$ - } -} +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/mybatis/dynamic/sql/select/package-info.java b/src/main/java/org/mybatis/dynamic/sql/select/package-info.java new file mode 100644 index 000000000..7e49fd4ec --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +@NullMarked +package org.mybatis.dynamic.sql.select; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/DefaultSelectStatementProvider.java b/src/main/java/org/mybatis/dynamic/sql/select/render/DefaultSelectStatementProvider.java index a56f19019..a79029144 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/DefaultSelectStatementProvider.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/DefaultSelectStatementProvider.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -20,43 +20,45 @@ import java.util.Map; import java.util.Objects; +import org.jspecify.annotations.Nullable; + public class DefaultSelectStatementProvider implements SelectStatementProvider { - private String selectStatement; - private Map parameters; - + private final String selectStatement; + private final Map parameters; + private DefaultSelectStatementProvider(Builder builder) { selectStatement = Objects.requireNonNull(builder.selectStatement); parameters = Collections.unmodifiableMap(Objects.requireNonNull(builder.parameters)); } - + @Override public Map getParameters() { return parameters; } - + @Override public String getSelectStatement() { return selectStatement; } - + public static Builder withSelectStatement(String selectStatement) { return new Builder().withSelectStatement(selectStatement); } - + public static class Builder { - private String selectStatement; - private Map parameters = new HashMap<>(); - + private @Nullable String selectStatement; + private final Map parameters = new HashMap<>(); + public Builder withSelectStatement(String selectStatement) { this.selectStatement = selectStatement; return this; } - + public Builder withParameters(Map parameters) { this.parameters.putAll(parameters); return this; } - + public DefaultSelectStatementProvider build() { return new DefaultSelectStatementProvider(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/FetchFirstPagingModelRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/FetchFirstPagingModelRenderer.java index 622c00d78..31bbd9cb8 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/FetchFirstPagingModelRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/FetchFirstPagingModelRenderer.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,71 +15,66 @@ */ package org.mybatis.dynamic.sql.select.render; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicInteger; - -import org.mybatis.dynamic.sql.render.RenderingStrategy; +import org.mybatis.dynamic.sql.exception.InvalidSqlException; +import org.mybatis.dynamic.sql.render.RenderedParameterInfo; +import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.select.PagingModel; import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.InternalError; +import org.mybatis.dynamic.sql.util.Messages; public class FetchFirstPagingModelRenderer { - private RenderingStrategy renderingStrategy; - private PagingModel pagingModel; - private AtomicInteger sequence; + private final RenderingContext renderingContext; + private final PagingModel pagingModel; - public FetchFirstPagingModelRenderer(RenderingStrategy renderingStrategy, - PagingModel pagingModel, AtomicInteger sequence) { - this.renderingStrategy = renderingStrategy; + public FetchFirstPagingModelRenderer(RenderingContext renderingContext, PagingModel pagingModel) { + this.renderingContext = renderingContext; this.pagingModel = pagingModel; - this.sequence = sequence; } - public Optional render() { + public FragmentAndParameters render() { return pagingModel.offset() .map(this::renderWithOffset) .orElseGet(this::renderFetchFirstRowsOnly); } - private Optional renderWithOffset(Long offset) { + private FragmentAndParameters renderWithOffset(Long offset) { return pagingModel.fetchFirstRows() .map(ffr -> renderOffsetAndFetchFirstRows(offset, ffr)) .orElseGet(() -> renderOffsetOnly(offset)); } - private Optional renderFetchFirstRowsOnly() { - return pagingModel.fetchFirstRows().flatMap(this::renderFetchFirstRowsOnly); + private FragmentAndParameters renderFetchFirstRowsOnly() { + return pagingModel.fetchFirstRows().map(this::renderFetchFirstRowsOnly) + .orElseThrow(() -> + new InvalidSqlException(Messages.getInternalErrorString(InternalError.INTERNAL_ERROR_13))); } - private Optional renderFetchFirstRowsOnly(Long fetchFirstRows) { - String mapKey = RenderingStrategy.formatParameterMapKey(sequence); + private FragmentAndParameters renderFetchFirstRowsOnly(Long fetchFirstRows) { + RenderedParameterInfo fetchFirstParameterInfo = renderingContext.calculateFetchFirstRowsParameterInfo(); return FragmentAndParameters - .withFragment("fetch first " + renderPlaceholder(mapKey) //$NON-NLS-1$ + .withFragment("fetch first " + fetchFirstParameterInfo.renderedPlaceHolder() //$NON-NLS-1$ + " rows only") //$NON-NLS-1$ - .withParameter(mapKey, fetchFirstRows) - .buildOptional(); + .withParameter(fetchFirstParameterInfo.parameterMapKey(), fetchFirstRows) + .build(); } - private Optional renderOffsetOnly(Long offset) { - String mapKey = RenderingStrategy.formatParameterMapKey(sequence); - return FragmentAndParameters.withFragment("offset " + renderPlaceholder(mapKey) //$NON-NLS-1$ + private FragmentAndParameters renderOffsetOnly(Long offset) { + RenderedParameterInfo offsetParameterInfo = renderingContext.calculateOffsetParameterInfo(); + return FragmentAndParameters.withFragment("offset " + offsetParameterInfo.renderedPlaceHolder() //$NON-NLS-1$ + " rows") //$NON-NLS-1$ - .withParameter(mapKey, offset) - .buildOptional(); + .withParameter(offsetParameterInfo.parameterMapKey(), offset) + .build(); } - private Optional renderOffsetAndFetchFirstRows(Long offset, Long fetchFirstRows) { - String mapKey1 = RenderingStrategy.formatParameterMapKey(sequence); - String mapKey2 = RenderingStrategy.formatParameterMapKey(sequence); - return FragmentAndParameters.withFragment("offset " + renderPlaceholder(mapKey1) //$NON-NLS-1$ - + " rows fetch first " + renderPlaceholder(mapKey2) //$NON-NLS-1$ + private FragmentAndParameters renderOffsetAndFetchFirstRows(Long offset, Long fetchFirstRows) { + RenderedParameterInfo offsetParameterInfo = renderingContext.calculateOffsetParameterInfo(); + RenderedParameterInfo fetchFirstParameterInfo = renderingContext.calculateFetchFirstRowsParameterInfo(); + return FragmentAndParameters.withFragment("offset " + offsetParameterInfo.renderedPlaceHolder() //$NON-NLS-1$ + + " rows fetch first " + fetchFirstParameterInfo.renderedPlaceHolder() //$NON-NLS-1$ + " rows only") //$NON-NLS-1$ - .withParameter(mapKey1, offset) - .withParameter(mapKey2, fetchFirstRows) - .buildOptional(); - } - - private String renderPlaceholder(String parameterName) { - return renderingStrategy.getFormattedJdbcPlaceholder(RenderingStrategy.DEFAULT_PARAMETER_PREFIX, - parameterName); + .withParameter(offsetParameterInfo.parameterMapKey(), offset) + .withParameter(fetchFirstParameterInfo.parameterMapKey(), fetchFirstRows) + .build(); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/HavingRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/HavingRenderer.java new file mode 100644 index 000000000..f7feb8b3f --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/HavingRenderer.java @@ -0,0 +1,44 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select.render; + +import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionRenderer; +import org.mybatis.dynamic.sql.select.HavingModel; + +public class HavingRenderer extends AbstractBooleanExpressionRenderer { + private HavingRenderer(Builder builder) { + super("having", builder); //$NON-NLS-1$ + } + + public static Builder withHavingModel(HavingModel havingModel) { + return new Builder(havingModel); + } + + public static class Builder extends AbstractBuilder { + public Builder(HavingModel havingModel) { + super(havingModel); + } + + @Override + protected Builder getThis() { + return this; + } + + public HavingRenderer build() { + return new HavingRenderer(this); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/JoinRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/JoinRenderer.java index 4c009d467..c1e8a0387 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/JoinRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/JoinRenderer.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,73 +15,74 @@ */ package org.mybatis.dynamic.sql.select.render; -import static org.mybatis.dynamic.sql.util.StringUtilities.spaceAfter; -import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore; - import java.util.Objects; import java.util.stream.Collectors; -import org.mybatis.dynamic.sql.BasicColumn; -import org.mybatis.dynamic.sql.select.QueryExpressionModel; -import org.mybatis.dynamic.sql.select.join.JoinCriterion; +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.exception.InvalidSqlException; +import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.select.join.JoinModel; import org.mybatis.dynamic.sql.select.join.JoinSpecification; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.FragmentCollector; +import org.mybatis.dynamic.sql.util.Messages; public class JoinRenderer { - private JoinModel joinModel; - private QueryExpressionModel queryExpression; - + private final JoinModel joinModel; + private final TableExpressionRenderer tableExpressionRenderer; + private final RenderingContext renderingContext; + private JoinRenderer(Builder builder) { joinModel = Objects.requireNonNull(builder.joinModel); - queryExpression = Objects.requireNonNull(builder.queryExpression); - } - - public String render() { - return joinModel.mapJoinSpecifications(this::toRenderedString) - .collect(Collectors.joining(" ")); //$NON-NLS-1$ - } - - private String toRenderedString(JoinSpecification joinSpecification) { - return spaceAfter(joinSpecification.joinType().shortType()) - + "join" //$NON-NLS-1$ - + spaceBefore(queryExpression.calculateTableNameIncludingAlias(joinSpecification.table())) - + spaceBefore(renderConditions(joinSpecification)); + tableExpressionRenderer = Objects.requireNonNull(builder.tableExpressionRenderer); + renderingContext = Objects.requireNonNull(builder.renderingContext); } - - private String renderConditions(JoinSpecification joinSpecification) { - return joinSpecification.mapJoinCriteria(this::renderCriterion) - .collect(Collectors.joining(" ")); //$NON-NLS-1$ - } - - private String renderCriterion(JoinCriterion joinCriterion) { - return joinCriterion.connector() - + spaceBefore(applyTableAlias(joinCriterion.leftColumn())) - + spaceBefore(joinCriterion.operator()) - + spaceBefore(applyTableAlias(joinCriterion.rightColumn())); + + public FragmentAndParameters render() { + return joinModel.joinSpecifications() + .map(this::renderJoinSpecification) + .collect(FragmentCollector.collect()) + .toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ } - - private String applyTableAlias(BasicColumn column) { - return column.renderWithTableAlias(queryExpression.tableAliasCalculator()); + + private FragmentAndParameters renderJoinSpecification(JoinSpecification joinSpecification) { + FragmentCollector fc = new FragmentCollector(); + fc.add(FragmentAndParameters.fromFragment(joinSpecification.joinType().type())); + fc.add(joinSpecification.table().accept(tableExpressionRenderer)); + fc.add(JoinSpecificationRenderer + .withJoinSpecification(joinSpecification) + .withRenderingContext(renderingContext) + .build() + .render() + .orElseThrow(() -> new InvalidSqlException(Messages.getString("ERROR.46")))); //$NON-NLS-1$ + + return fc.toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ } - + public static Builder withJoinModel(JoinModel joinModel) { return new Builder().withJoinModel(joinModel); } - + public static class Builder { - private JoinModel joinModel; - private QueryExpressionModel queryExpression; - + private @Nullable JoinModel joinModel; + private @Nullable TableExpressionRenderer tableExpressionRenderer; + private @Nullable RenderingContext renderingContext; + public Builder withJoinModel(JoinModel joinModel) { this.joinModel = joinModel; return this; } - - public Builder withQueryExpression(QueryExpressionModel queryExpression) { - this.queryExpression = queryExpression; + + public Builder withTableExpressionRenderer(TableExpressionRenderer tableExpressionRenderer) { + this.tableExpressionRenderer = tableExpressionRenderer; + return this; + } + + public Builder withRenderingContext(RenderingContext renderingContext) { + this.renderingContext = renderingContext; return this; } - + public JoinRenderer build() { return new JoinRenderer(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/JoinSpecificationRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/JoinSpecificationRenderer.java new file mode 100644 index 000000000..97766c171 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/JoinSpecificationRenderer.java @@ -0,0 +1,44 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select.render; + +import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionRenderer; +import org.mybatis.dynamic.sql.select.join.JoinSpecification; + +public class JoinSpecificationRenderer extends AbstractBooleanExpressionRenderer { + private JoinSpecificationRenderer(Builder builder) { + super("on", builder); //$NON-NLS-1$ + } + + public static JoinSpecificationRenderer.Builder withJoinSpecification(JoinSpecification joinSpecification) { + return new Builder(joinSpecification); + } + + public static class Builder extends AbstractBuilder { + public Builder(JoinSpecification joinSpecification) { + super(joinSpecification); + } + + public JoinSpecificationRenderer build() { + return new JoinSpecificationRenderer(this); + } + + @Override + protected Builder getThis() { + return this; + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/LimitAndOffsetPagingModelRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/LimitAndOffsetPagingModelRenderer.java index 4ef0eb508..ea6452b67 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/LimitAndOffsetPagingModelRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/LimitAndOffsetPagingModelRenderer.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,51 +15,44 @@ */ package org.mybatis.dynamic.sql.select.render; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.Objects; -import org.mybatis.dynamic.sql.render.RenderingStrategy; +import org.mybatis.dynamic.sql.render.RenderedParameterInfo; +import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.select.PagingModel; import org.mybatis.dynamic.sql.util.FragmentAndParameters; public class LimitAndOffsetPagingModelRenderer { - private RenderingStrategy renderingStrategy; - private Long limit; - private PagingModel pagingModel; - private AtomicInteger sequence; - - public LimitAndOffsetPagingModelRenderer(RenderingStrategy renderingStrategy, - Long limit, PagingModel pagingModel, AtomicInteger sequence) { - this.renderingStrategy = renderingStrategy; - this.limit = limit; + private final RenderingContext renderingContext; + private final Long limit; + private final PagingModel pagingModel; + + public LimitAndOffsetPagingModelRenderer(RenderingContext renderingContext, + Long limit, PagingModel pagingModel) { + this.renderingContext = renderingContext; + this.limit = Objects.requireNonNull(limit); this.pagingModel = pagingModel; - this.sequence = sequence; } - public Optional render() { + public FragmentAndParameters render() { return pagingModel.offset().map(this::renderLimitAndOffset) .orElseGet(this::renderLimitOnly); } - private Optional renderLimitOnly() { - String mapKey = RenderingStrategy.formatParameterMapKey(sequence); - return FragmentAndParameters.withFragment("limit " + renderPlaceholder(mapKey)) //$NON-NLS-1$ - .withParameter(mapKey, limit) - .buildOptional(); - } - - private Optional renderLimitAndOffset(Long offset) { - String mapKey1 = RenderingStrategy.formatParameterMapKey(sequence); - String mapKey2 = RenderingStrategy.formatParameterMapKey(sequence); - return FragmentAndParameters.withFragment("limit " + renderPlaceholder(mapKey1) //$NON-NLS-1$ - + " offset " + renderPlaceholder(mapKey2)) //$NON-NLS-1$ - .withParameter(mapKey1, limit) - .withParameter(mapKey2, offset) - .buildOptional(); + private FragmentAndParameters renderLimitOnly() { + RenderedParameterInfo limitParameterInfo = renderingContext.calculateLimitParameterInfo(); + return FragmentAndParameters.withFragment("limit " + limitParameterInfo.renderedPlaceHolder()) //$NON-NLS-1$ + .withParameter(limitParameterInfo.parameterMapKey(), limit) + .build(); } - private String renderPlaceholder(String parameterName) { - return renderingStrategy.getFormattedJdbcPlaceholder(RenderingStrategy.DEFAULT_PARAMETER_PREFIX, - parameterName); + private FragmentAndParameters renderLimitAndOffset(Long offset) { + RenderedParameterInfo limitParameterInfo = renderingContext.calculateLimitParameterInfo(); + RenderedParameterInfo offsetParameterInfo = renderingContext.calculateOffsetParameterInfo(); + return FragmentAndParameters.withFragment("limit " + limitParameterInfo.renderedPlaceHolder() //$NON-NLS-1$ + + " offset " + offsetParameterInfo.renderedPlaceHolder()) //$NON-NLS-1$ + .withParameter(limitParameterInfo.parameterMapKey(), limit) + .withParameter(offsetParameterInfo.parameterMapKey(), offset) + .build(); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/MultiSelectRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/MultiSelectRenderer.java new file mode 100644 index 000000000..4daf576d4 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/MultiSelectRenderer.java @@ -0,0 +1,127 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select.render; + +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.common.OrderByModel; +import org.mybatis.dynamic.sql.common.OrderByRenderer; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.render.RenderingStrategy; +import org.mybatis.dynamic.sql.select.MultiSelectModel; +import org.mybatis.dynamic.sql.select.PagingModel; +import org.mybatis.dynamic.sql.select.SelectModel; +import org.mybatis.dynamic.sql.select.UnionQuery; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.FragmentCollector; + +public class MultiSelectRenderer { + private final MultiSelectModel multiSelectModel; + private final RenderingContext renderingContext; + + private MultiSelectRenderer(Builder builder) { + multiSelectModel = Objects.requireNonNull(builder.multiSelectModel); + renderingContext = RenderingContext + .withRenderingStrategy(Objects.requireNonNull(builder.renderingStrategy)) + .withStatementConfiguration(multiSelectModel.statementConfiguration()) + .build(); + } + + public SelectStatementProvider render() { + FragmentAndParameters initialSelect = renderSelect(multiSelectModel.initialSelect()); + + FragmentCollector fragmentCollector = multiSelectModel + .unionQueries() + .map(this::renderSelect) + .collect(FragmentCollector.collect(initialSelect)); + + renderOrderBy().ifPresent(fragmentCollector::add); + renderPagingModel().ifPresent(fragmentCollector::add); + + return toSelectStatementProvider(fragmentCollector); + } + + private SelectStatementProvider toSelectStatementProvider(FragmentCollector fragmentCollector) { + return DefaultSelectStatementProvider + .withSelectStatement(fragmentCollector.collectFragments(Collectors.joining(" "))) //$NON-NLS-1$ + .withParameters(fragmentCollector.parameters()) + .build(); + } + + private FragmentAndParameters renderSelect(SelectModel selectModel) { + return SubQueryRenderer.withSelectModel(selectModel) + .withRenderingContext(renderingContext) + .withPrefix("(") //$NON-NLS-1$ + .withSuffix(")") //$NON-NLS-1$ + .build() + .render(); + } + + private FragmentAndParameters renderSelect(UnionQuery unionQuery) { + return SubQueryRenderer.withSelectModel(unionQuery.selectModel()) + .withRenderingContext(renderingContext) + .withPrefix(unionQuery.connector() + " (") //$NON-NLS-1$ + .withSuffix(")") //$NON-NLS-1$ + .build() + .render(); + } + + private Optional renderOrderBy() { + return multiSelectModel.orderByModel().map(this::renderOrderBy); + } + + private FragmentAndParameters renderOrderBy(OrderByModel orderByModel) { + return new OrderByRenderer(renderingContext).render(orderByModel); + } + + private Optional renderPagingModel() { + return multiSelectModel.pagingModel().map(this::renderPagingModel); + } + + private FragmentAndParameters renderPagingModel(PagingModel pagingModel) { + return new PagingModelRenderer.Builder() + .withPagingModel(pagingModel) + .withRenderingContext(renderingContext) + .build() + .render(); + } + + public static Builder withMultiSelectModel(MultiSelectModel multiSelectModel) { + return new Builder().withMultiSelectModel(multiSelectModel); + } + + public static class Builder { + private @Nullable RenderingStrategy renderingStrategy; + private @Nullable MultiSelectModel multiSelectModel; + + public Builder withRenderingStrategy(RenderingStrategy renderingStrategy) { + this.renderingStrategy = renderingStrategy; + return this; + } + + public Builder withMultiSelectModel(MultiSelectModel multiSelectModel) { + this.multiSelectModel = multiSelectModel; + return this; + } + + public MultiSelectRenderer build() { + return new MultiSelectRenderer(this); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/PagingModelRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/PagingModelRenderer.java index cdcb54ab5..6b79de960 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/PagingModelRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/PagingModelRenderer.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -16,45 +16,40 @@ package org.mybatis.dynamic.sql.select.render; import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicInteger; -import org.mybatis.dynamic.sql.render.RenderingStrategy; +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.select.PagingModel; import org.mybatis.dynamic.sql.util.FragmentAndParameters; public class PagingModelRenderer { - private RenderingStrategy renderingStrategy; - private PagingModel pagingModel; - private AtomicInteger sequence; + private final PagingModel pagingModel; + private final RenderingContext renderingContext; private PagingModelRenderer(Builder builder) { - renderingStrategy = Objects.requireNonNull(builder.renderingStrategy); + renderingContext = Objects.requireNonNull(builder.renderingContext); pagingModel = Objects.requireNonNull(builder.pagingModel); - sequence = Objects.requireNonNull(builder.sequence); } - public Optional render() { + public FragmentAndParameters render() { return pagingModel.limit().map(this::limitAndOffsetRender) .orElseGet(this::fetchFirstRender); } - private Optional limitAndOffsetRender(Long limit) { - return new LimitAndOffsetPagingModelRenderer(renderingStrategy, limit, - pagingModel, sequence).render(); + private FragmentAndParameters limitAndOffsetRender(Long limit) { + return new LimitAndOffsetPagingModelRenderer(renderingContext, limit, pagingModel).render(); } - private Optional fetchFirstRender() { - return new FetchFirstPagingModelRenderer(renderingStrategy, pagingModel, sequence).render(); + private FragmentAndParameters fetchFirstRender() { + return new FetchFirstPagingModelRenderer(renderingContext, pagingModel).render(); } public static class Builder { - private RenderingStrategy renderingStrategy; - private PagingModel pagingModel; - private AtomicInteger sequence; + private @Nullable PagingModel pagingModel; + private @Nullable RenderingContext renderingContext; - public Builder withRenderingStrategy(RenderingStrategy renderingStrategy) { - this.renderingStrategy = renderingStrategy; + public Builder withRenderingContext(RenderingContext renderingContext) { + this.renderingContext = renderingContext; return this; } @@ -63,11 +58,6 @@ public Builder withPagingModel(PagingModel pagingModel) { return this; } - public Builder withSequence(AtomicInteger sequence) { - this.sequence = sequence; - return this; - } - public PagingModelRenderer build() { return new PagingModelRenderer(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/QueryExpressionRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/QueryExpressionRenderer.java index a2e3db616..610ba6d10 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/QueryExpressionRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/QueryExpressionRenderer.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,138 +15,205 @@ */ package org.mybatis.dynamic.sql.select.render; -import static org.mybatis.dynamic.sql.util.StringUtilities.spaceAfter; -import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore; - import java.util.Objects; import java.util.Optional; -import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; import org.mybatis.dynamic.sql.BasicColumn; -import org.mybatis.dynamic.sql.SqlTable; -import org.mybatis.dynamic.sql.render.RenderingStrategy; +import org.mybatis.dynamic.sql.TableExpression; +import org.mybatis.dynamic.sql.render.ExplicitTableAliasCalculator; +import org.mybatis.dynamic.sql.render.GuaranteedTableAliasCalculator; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.render.TableAliasCalculator; import org.mybatis.dynamic.sql.select.GroupByModel; +import org.mybatis.dynamic.sql.select.HavingModel; import org.mybatis.dynamic.sql.select.QueryExpressionModel; import org.mybatis.dynamic.sql.select.join.JoinModel; -import org.mybatis.dynamic.sql.util.CustomCollectors; import org.mybatis.dynamic.sql.util.FragmentAndParameters; -import org.mybatis.dynamic.sql.where.WhereModel; -import org.mybatis.dynamic.sql.where.render.WhereClauseProvider; -import org.mybatis.dynamic.sql.where.render.WhereRenderer; +import org.mybatis.dynamic.sql.util.FragmentCollector; +import org.mybatis.dynamic.sql.util.StringUtilities; +import org.mybatis.dynamic.sql.where.EmbeddedWhereModel; public class QueryExpressionRenderer { - private QueryExpressionModel queryExpression; - private RenderingStrategy renderingStrategy; - private AtomicInteger sequence; - + private final QueryExpressionModel queryExpression; + private final TableExpressionRenderer tableExpressionRenderer; + private final RenderingContext renderingContext; + private QueryExpressionRenderer(Builder builder) { queryExpression = Objects.requireNonNull(builder.queryExpression); - renderingStrategy = Objects.requireNonNull(builder.renderingStrategy); - sequence = Objects.requireNonNull(builder.sequence); - } - - public FragmentAndParameters render() { - return queryExpression.whereModel() - .flatMap(this::renderWhereClause) - .map(this::renderWithWhereClause) - .orElseGet(this::renderWithoutWhereClause); - } - - private FragmentAndParameters renderWithWhereClause(WhereClauseProvider whereClause) { - return FragmentAndParameters.withFragment(calculateQueryExpression(whereClause)) - .withParameters(whereClause.getParameters()) + TableAliasCalculator childTableAliasCalculator = calculateChildTableAliasCalculator(queryExpression); + + renderingContext = Objects.requireNonNull(builder.renderingContext) + .withChildTableAliasCalculator(childTableAliasCalculator); + + tableExpressionRenderer = new TableExpressionRenderer.Builder() + .withRenderingContext(renderingContext) .build(); } - - private FragmentAndParameters renderWithoutWhereClause() { - return FragmentAndParameters.withFragment(calculateQueryExpression()) - .build(); + + /** + * This function calculates a table alias calculator to use in the current context. There are several + * possibilities: this could be a renderer for a top level select statement, or it could be a renderer for a table + * expression in a join, or a column to sub query where condition, or it could be a renderer for a select + * statement in an "exists" condition in a where clause. + * + *

In the case of conditions in a where clause, we will have a parent table alias calculator. This will give + * visibility to the aliases in the outer select statement to this renderer so columns in aliased tables can be + * used in where clause sub query conditions without having to re-specify the alias. + * + *

Another complication is that we calculate aliases differently if there are joins and sub queries. The + * cases are as follows: + * + *

    + *
  1. If there are no joins, then we will only use aliases that are explicitly set by the user
  2. + *
  3. If there are joins and sub queries, we will also only use explicit aliases
  4. + *
  5. If there are joins, but no sub queries, then we will automatically use the table name + * as an alias if no explicit alias has been specified
  6. + *
+ * + * @param queryExpression the model to render + * @return a table alias calculator appropriate for this context + */ + private TableAliasCalculator calculateChildTableAliasCalculator(QueryExpressionModel queryExpression) { + return queryExpression.joinModel() + .map(JoinModel::containsSubQueries) + .map(this::calculateTableAliasCalculatorWithJoins) + .orElseGet(this::explicitTableAliasCalculator); + } + + private TableAliasCalculator calculateTableAliasCalculatorWithJoins(boolean hasSubQueries) { + if (hasSubQueries) { + // if there are subqueries, we cannot use the table name automatically + // so all aliases must be specified + return explicitTableAliasCalculator(); + } else { + // without subqueries, we can automatically use table names as aliases + return guaranteedTableAliasCalculator(); + } + } + + private TableAliasCalculator explicitTableAliasCalculator() { + return ExplicitTableAliasCalculator.of(queryExpression.tableAliases()); } - private String calculateQueryExpression() { - return calculateQueryExpressionStart() - + spaceBefore(queryExpression.groupByModel().map(this::renderGroupBy)); + private TableAliasCalculator guaranteedTableAliasCalculator() { + return GuaranteedTableAliasCalculator.of(queryExpression.tableAliases()); } - private String calculateQueryExpression(WhereClauseProvider whereClause) { - return calculateQueryExpressionStart() - + spaceBefore(whereClause.getWhereClause()) - + spaceBefore(queryExpression.groupByModel().map(this::renderGroupBy)); + public FragmentAndParameters render() { + FragmentCollector fragmentCollector = new FragmentCollector(); + + fragmentCollector.add(calculateQueryExpressionStart()); + calculateJoinClause().ifPresent(fragmentCollector::add); + calculateWhereClause().ifPresent(fragmentCollector::add); + calculateGroupByClause().ifPresent(fragmentCollector::add); + calculateHavingClause().ifPresent(fragmentCollector::add); + + return fragmentCollector.toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ } - private String calculateQueryExpressionStart() { - return spaceAfter(queryExpression.connector()) + private FragmentAndParameters calculateQueryExpressionStart() { + FragmentAndParameters columnList = calculateColumnList(); + + String start = queryExpression.connector().map(StringUtilities::spaceAfter).orElse("") //$NON-NLS-1$ + "select " //$NON-NLS-1$ + (queryExpression.isDistinct() ? "distinct " : "") //$NON-NLS-1$ //$NON-NLS-2$ - + calculateColumnList() - + " from " //$NON-NLS-1$ - + calculateTableName(queryExpression.table()) - + spaceBefore(queryExpression.joinModel().map(this::renderJoin)); + + columnList.fragment() + + " from "; //$NON-NLS-1$ + + FragmentAndParameters renderedTable = renderTableExpression(queryExpression.table()); + start += renderedTable.fragment(); + + return FragmentAndParameters.withFragment(start) + .withParameters(renderedTable.parameters()) + .withParameters(columnList.parameters()) + .build(); } - - private String calculateColumnList() { - return queryExpression.mapColumns(this::applyTableAndColumnAlias) - .collect(Collectors.joining(", ")); //$NON-NLS-1$ + + private FragmentAndParameters calculateColumnList() { + return queryExpression.columns() + .map(this::renderColumnAndAlias) + .collect(FragmentCollector.collect()) + .toFragmentAndParameters(Collectors.joining(", ")); //$NON-NLS-1$ + } + + private FragmentAndParameters renderColumnAndAlias(BasicColumn selectListItem) { + FragmentAndParameters renderedColumn = selectListItem.render(renderingContext); + + return selectListItem.alias().map(a -> renderedColumn.mapFragment(f -> f + " as " + a)) //$NON-NLS-1$ + .orElse(renderedColumn); } - private String calculateTableName(SqlTable table) { - return queryExpression.calculateTableNameIncludingAlias(table); + private FragmentAndParameters renderTableExpression(TableExpression table) { + return table.accept(tableExpressionRenderer); } - - private String applyTableAndColumnAlias(BasicColumn selectListItem) { - return selectListItem.renderWithTableAndColumnAlias(queryExpression.tableAliasCalculator()); + + private Optional calculateJoinClause() { + return queryExpression.joinModel().map(this::renderJoin); } - - private String renderJoin(JoinModel joinModel) { + + private FragmentAndParameters renderJoin(JoinModel joinModel) { return JoinRenderer.withJoinModel(joinModel) - .withQueryExpression(queryExpression) + .withTableExpressionRenderer(tableExpressionRenderer) + .withRenderingContext(renderingContext) .build() .render(); } - - private Optional renderWhereClause(WhereModel whereModel) { - return WhereRenderer.withWhereModel(whereModel) - .withRenderingStrategy(renderingStrategy) - .withTableAliasCalculator(queryExpression.tableAliasCalculator()) - .withSequence(sequence) - .build() - .render(); + + private Optional calculateWhereClause() { + return queryExpression.whereModel().flatMap(this::renderWhereClause); } - private String renderGroupBy(GroupByModel groupByModel) { - return groupByModel.mapColumns(this::applyTableAlias) - .collect(CustomCollectors.joining(", ", "group by ", "")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + private Optional renderWhereClause(EmbeddedWhereModel whereModel) { + return whereModel.render(renderingContext); } - - private String applyTableAlias(BasicColumn column) { - return column.renderWithTableAlias(queryExpression.tableAliasCalculator()); + + private Optional calculateGroupByClause() { + return queryExpression.groupByModel().map(this::renderGroupBy); } - + + private FragmentAndParameters renderGroupBy(GroupByModel groupByModel) { + return groupByModel.columns() + .map(this::renderColumn) + .collect(FragmentCollector.collect()) + .toFragmentAndParameters( + Collectors.joining(", ", "group by ", "")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$) + } + + private FragmentAndParameters renderColumn(BasicColumn column) { + return column.render(renderingContext); + } + + private Optional calculateHavingClause() { + return queryExpression.havingModel().flatMap(this::renderHavingClause); + } + + private Optional renderHavingClause(HavingModel havingModel) { + return HavingRenderer.withHavingModel(havingModel) + .withRenderingContext(renderingContext) + .build() + .render(); + } + public static Builder withQueryExpression(QueryExpressionModel model) { return new Builder().withQueryExpression(model); } - + public static class Builder { - private QueryExpressionModel queryExpression; - private RenderingStrategy renderingStrategy; - private AtomicInteger sequence; - - public Builder withQueryExpression(QueryExpressionModel queryExpression) { - this.queryExpression = queryExpression; - return this; - } - - public Builder withRenderingStrategy(RenderingStrategy renderingStrategy) { - this.renderingStrategy = renderingStrategy; + private @Nullable QueryExpressionModel queryExpression; + private @Nullable RenderingContext renderingContext; + + public Builder withRenderingContext(RenderingContext renderingContext) { + this.renderingContext = renderingContext; return this; } - - public Builder withSequence(AtomicInteger sequence) { - this.sequence = sequence; + + public Builder withQueryExpression(QueryExpressionModel queryExpression) { + this.queryExpression = queryExpression; return this; } - + public QueryExpressionRenderer build() { return new QueryExpressionRenderer(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseRenderer.java new file mode 100644 index 000000000..9fd58a591 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseRenderer.java @@ -0,0 +1,89 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select.render; + +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.exception.InvalidSqlException; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseModel; +import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseWhenCondition; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.FragmentCollector; +import org.mybatis.dynamic.sql.util.Messages; + +public class SearchedCaseRenderer { + private final SearchedCaseModel searchedCaseModel; + private final RenderingContext renderingContext; + + public SearchedCaseRenderer(SearchedCaseModel searchedCaseModel, RenderingContext renderingContext) { + this.searchedCaseModel = Objects.requireNonNull(searchedCaseModel); + this.renderingContext = Objects.requireNonNull(renderingContext); + } + + public FragmentAndParameters render() { + FragmentCollector fc = new FragmentCollector(); + fc.add(renderCase()); + fc.add(renderWhenConditions()); + renderElse().ifPresent(fc::add); + fc.add(renderEnd()); + return fc.toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ + } + + private FragmentAndParameters renderCase() { + return FragmentAndParameters.fromFragment("case"); //$NON-NLS-1$ + } + + private FragmentAndParameters renderWhenConditions() { + return searchedCaseModel.whenConditions().map(this::renderWhenCondition) + .collect(FragmentCollector.collect()) + .toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ + } + + private FragmentAndParameters renderWhenCondition(SearchedCaseWhenCondition whenCondition) { + return Stream.of(renderWhen(whenCondition), renderThen(whenCondition)).collect(FragmentCollector.collect()) + .toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ + } + + private FragmentAndParameters renderWhen(SearchedCaseWhenCondition whenCondition) { + SearchedCaseWhenConditionRenderer renderer = new SearchedCaseWhenConditionRenderer.Builder(whenCondition) + .withRenderingContext(renderingContext) + .build(); + + return renderer.render() + .orElseThrow(() -> new InvalidSqlException(Messages.getString("ERROR.39"))); //$NON-NLS-1$ + } + + private FragmentAndParameters renderThen(SearchedCaseWhenCondition whenCondition) { + return whenCondition.thenValue().render(renderingContext).mapFragment(f -> "then " + f); + } + + private Optional renderElse() { + return searchedCaseModel.elseValue().map(this::renderElse); + } + + private FragmentAndParameters renderElse(BasicColumn elseValue) { + return elseValue.render(renderingContext).mapFragment(f -> "else " + f); //$NON-NLS-1$ + } + + private FragmentAndParameters renderEnd() { + return FragmentAndParameters.fromFragment("end"); //$NON-NLS-1$ + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseWhenConditionRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseWhenConditionRenderer.java new file mode 100644 index 000000000..f73150f5f --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseWhenConditionRenderer.java @@ -0,0 +1,42 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select.render; + +import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionRenderer; +import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseWhenCondition; + +public class SearchedCaseWhenConditionRenderer extends AbstractBooleanExpressionRenderer { + protected SearchedCaseWhenConditionRenderer(Builder builder) { + super("when", builder); + } + + public static class Builder + extends AbstractBooleanExpressionRenderer.AbstractBuilder { + + protected Builder(SearchedCaseWhenCondition model) { + super(model); + } + + public SearchedCaseWhenConditionRenderer build() { + return new SearchedCaseWhenConditionRenderer(this); + } + + @Override + protected Builder getThis() { + return this; + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/SelectRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/SelectRenderer.java index 9459b3dac..719d6aafe 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/SelectRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SelectRenderer.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -16,83 +16,35 @@ package org.mybatis.dynamic.sql.select.render; import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; -import org.mybatis.dynamic.sql.SortSpecification; +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.render.RenderingStrategy; -import org.mybatis.dynamic.sql.select.OrderByModel; -import org.mybatis.dynamic.sql.select.PagingModel; -import org.mybatis.dynamic.sql.select.QueryExpressionModel; import org.mybatis.dynamic.sql.select.SelectModel; -import org.mybatis.dynamic.sql.util.CustomCollectors; import org.mybatis.dynamic.sql.util.FragmentAndParameters; -import org.mybatis.dynamic.sql.util.FragmentCollector; public class SelectRenderer { - private SelectModel selectModel; - private RenderingStrategy renderingStrategy; - private AtomicInteger sequence; + private final SelectModel selectModel; + private final RenderingStrategy renderingStrategy; private SelectRenderer(Builder builder) { selectModel = Objects.requireNonNull(builder.selectModel); renderingStrategy = Objects.requireNonNull(builder.renderingStrategy); - sequence = builder.sequence().orElseGet(() -> new AtomicInteger(1)); } public SelectStatementProvider render() { - FragmentCollector fragmentCollector = selectModel - .mapQueryExpressions(this::renderQueryExpression) - .collect(FragmentCollector.collect()); - renderOrderBy(fragmentCollector); - renderPagingModel(fragmentCollector); - - String selectStatement = fragmentCollector.fragments().collect(Collectors.joining(" ")); //$NON-NLS-1$ - - return DefaultSelectStatementProvider.withSelectStatement(selectStatement) - .withParameters(fragmentCollector.parameters()) + RenderingContext renderingContext = RenderingContext.withRenderingStrategy(renderingStrategy) + .withStatementConfiguration(selectModel.statementConfiguration()) .build(); - } - private FragmentAndParameters renderQueryExpression(QueryExpressionModel queryExpressionModel) { - return QueryExpressionRenderer.withQueryExpression(queryExpressionModel) - .withRenderingStrategy(renderingStrategy) - .withSequence(sequence) + FragmentAndParameters fragmentAndParameters = SubQueryRenderer.withSelectModel(selectModel) + .withRenderingContext(renderingContext) .build() .render(); - } - - private void renderOrderBy(FragmentCollector fragmentCollector) { - selectModel.orderByModel().ifPresent(om -> renderOrderBy(fragmentCollector, om)); - } - - private void renderOrderBy(FragmentCollector fragmentCollector, OrderByModel orderByModel) { - String phrase = orderByModel.mapColumns(this::calculateOrderByPhrase) - .collect(CustomCollectors.joining(", ", "order by ", "")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - fragmentCollector.add(FragmentAndParameters.withFragment(phrase).build()); - } - - private String calculateOrderByPhrase(SortSpecification column) { - String phrase = column.aliasOrName(); - if (column.isDescending()) { - phrase = phrase + " DESC"; //$NON-NLS-1$ - } - return phrase; - } - - private void renderPagingModel(FragmentCollector fragmentCollector) { - selectModel.pagingModel().flatMap(this::renderPagingModel) - .ifPresent(fragmentCollector::add); - } - private Optional renderPagingModel(PagingModel pagingModel) { - return new PagingModelRenderer.Builder() - .withPagingModel(pagingModel) - .withRenderingStrategy(renderingStrategy) - .withSequence(sequence) - .build() - .render(); + return DefaultSelectStatementProvider.withSelectStatement(fragmentAndParameters.fragment()) + .withParameters(fragmentAndParameters.parameters()) + .build(); } public static Builder withSelectModel(SelectModel selectModel) { @@ -100,29 +52,19 @@ public static Builder withSelectModel(SelectModel selectModel) { } public static class Builder { - private SelectModel selectModel; - private RenderingStrategy renderingStrategy; - private AtomicInteger sequence; - - public Builder withSelectModel(SelectModel selectModel) { - this.selectModel = selectModel; - return this; - } + private @Nullable SelectModel selectModel; + private @Nullable RenderingStrategy renderingStrategy; public Builder withRenderingStrategy(RenderingStrategy renderingStrategy) { this.renderingStrategy = renderingStrategy; return this; } - public Builder withSequence(AtomicInteger sequence) { - this.sequence = sequence; + public Builder withSelectModel(SelectModel selectModel) { + this.selectModel = selectModel; return this; } - private Optional sequence() { - return Optional.ofNullable(sequence); - } - public SelectRenderer build() { return new SelectRenderer(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/SelectStatementProvider.java b/src/main/java/org/mybatis/dynamic/sql/select/render/SelectStatementProvider.java index 44b169e54..42cec3f55 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/SelectStatementProvider.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SelectStatementProvider.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java new file mode 100644 index 000000000..036a9c909 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java @@ -0,0 +1,96 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select.render; + +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseModel; +import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseWhenCondition; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.FragmentCollector; + +public class SimpleCaseRenderer { + private final SimpleCaseModel simpleCaseModel; + private final RenderingContext renderingContext; + private final SimpleCaseWhenConditionRenderer whenConditionRenderer; + + public SimpleCaseRenderer(SimpleCaseModel simpleCaseModel, RenderingContext renderingContext) { + this.simpleCaseModel = Objects.requireNonNull(simpleCaseModel); + this.renderingContext = Objects.requireNonNull(renderingContext); + whenConditionRenderer = new SimpleCaseWhenConditionRenderer<>(renderingContext, simpleCaseModel.column()); + } + + public FragmentAndParameters render() { + FragmentCollector fc = new FragmentCollector(); + fc.add(renderCase()); + fc.add(renderWhenConditions()); + renderElse().ifPresent(fc::add); + fc.add(renderEnd()); + return fc.toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ + } + + private FragmentAndParameters renderCase() { + return simpleCaseModel.column().alias() + .map(FragmentAndParameters::fromFragment) + .orElseGet(() -> simpleCaseModel.column().render(renderingContext)) + .mapFragment(f -> "case " + f); //$NON-NLS-1$ + } + + private FragmentAndParameters renderWhenConditions() { + return simpleCaseModel.whenConditions().map(this::renderWhenCondition) + .collect(FragmentCollector.collect()) + .toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ + } + + private FragmentAndParameters renderWhenCondition(SimpleCaseWhenCondition whenCondition) { + return Stream.of( + renderWhen(), + renderConditions(whenCondition), + renderThen(whenCondition) + ).collect(FragmentCollector.collect()) + .toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ + } + + private FragmentAndParameters renderWhen() { + return FragmentAndParameters.fromFragment("when"); //$NON-NLS-1$ + } + + private FragmentAndParameters renderConditions(SimpleCaseWhenCondition whenCondition) { + return whenCondition.accept(whenConditionRenderer); + } + + private FragmentAndParameters renderThen(SimpleCaseWhenCondition whenCondition) { + return whenCondition.thenValue().render(renderingContext) + .mapFragment(f -> "then " + f); //$NON-NLS-1$ + } + + private Optional renderElse() { + return simpleCaseModel.elseValue().map(this::renderElse); + } + + private FragmentAndParameters renderElse(BasicColumn elseValue) { + return elseValue.render(renderingContext).mapFragment(f -> "else " + f); //$NON-NLS-1$ + } + + private FragmentAndParameters renderEnd() { + return FragmentAndParameters.fromFragment("end"); //$NON-NLS-1$ + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseWhenConditionRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseWhenConditionRenderer.java new file mode 100644 index 000000000..1bb6e2adc --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseWhenConditionRenderer.java @@ -0,0 +1,74 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select.render; + +import java.util.Objects; +import java.util.stream.Collectors; + +import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.RenderableCondition; +import org.mybatis.dynamic.sql.render.RenderedParameterInfo; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.select.caseexpression.BasicWhenCondition; +import org.mybatis.dynamic.sql.select.caseexpression.ConditionBasedWhenCondition; +import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseWhenConditionVisitor; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.FragmentCollector; +import org.mybatis.dynamic.sql.util.Validator; + +public class SimpleCaseWhenConditionRenderer implements SimpleCaseWhenConditionVisitor { + private final RenderingContext renderingContext; + private final BindableColumn column; + + public SimpleCaseWhenConditionRenderer(RenderingContext renderingContext, BindableColumn column) { + this.renderingContext = Objects.requireNonNull(renderingContext); + this.column = Objects.requireNonNull(column); + } + + @Override + public FragmentAndParameters visit(ConditionBasedWhenCondition whenCondition) { + FragmentCollector fragmentCollector = whenCondition.conditions() + .filter(this::shouldRender) + .map(this::renderCondition) + .collect(FragmentCollector.collect()); + + Validator.assertFalse(fragmentCollector.isEmpty(), "ERROR.39"); //$NON-NLS-1$ + + return fragmentCollector.toFragmentAndParameters(Collectors.joining(", ")); //$NON-NLS-1$ + } + + @Override + public FragmentAndParameters visit(BasicWhenCondition whenCondition) { + return whenCondition.conditions().map(this::renderBasicValue) + .collect(FragmentCollector.collect()) + .toFragmentAndParameters(Collectors.joining(", ")); //$NON-NLS-1$ + } + + private boolean shouldRender(RenderableCondition condition) { + return condition.shouldRender(renderingContext); + } + + private FragmentAndParameters renderCondition(RenderableCondition condition) { + return condition.renderCondition(renderingContext, column); + } + + private FragmentAndParameters renderBasicValue(T value) { + RenderedParameterInfo rpi = renderingContext.calculateParameterInfo(column); + return FragmentAndParameters.withFragment(rpi.renderedPlaceHolder()) + .withParameter(rpi.parameterMapKey(), value) + .build(); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/SubQueryRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/SubQueryRenderer.java new file mode 100644 index 000000000..359e9c1f0 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SubQueryRenderer.java @@ -0,0 +1,122 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select.render; + +import java.util.Objects; +import java.util.stream.Collectors; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.common.OrderByModel; +import org.mybatis.dynamic.sql.common.OrderByRenderer; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.select.PagingModel; +import org.mybatis.dynamic.sql.select.QueryExpressionModel; +import org.mybatis.dynamic.sql.select.SelectModel; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.FragmentCollector; + +public class SubQueryRenderer { + private final SelectModel selectModel; + private final RenderingContext renderingContext; + private final String prefix; + private final String suffix; + + private SubQueryRenderer(Builder builder) { + selectModel = Objects.requireNonNull(builder.selectModel); + renderingContext = Objects.requireNonNull(builder.renderingContext); + prefix = builder.prefix == null ? "" : builder.prefix; //$NON-NLS-1$ + suffix = builder.suffix == null ? "" : builder.suffix; //$NON-NLS-1$ + } + + public FragmentAndParameters render() { + FragmentCollector fragmentCollector = selectModel + .queryExpressions() + .map(this::renderQueryExpression) + .collect(FragmentCollector.collect()); + + selectModel.orderByModel() + .map(this::renderOrderBy) + .ifPresent(fragmentCollector::add); + + selectModel.pagingModel() + .map(this::renderPagingModel) + .ifPresent(fragmentCollector::add); + + selectModel.forClause() + .map(FragmentAndParameters::fromFragment) + .ifPresent(fragmentCollector::add); + + selectModel.waitClause() + .map(FragmentAndParameters::fromFragment) + .ifPresent(fragmentCollector::add); + + return fragmentCollector.toFragmentAndParameters(Collectors.joining(" ", prefix, suffix)); //$NON-NLS-1$ + } + + private FragmentAndParameters renderQueryExpression(QueryExpressionModel queryExpressionModel) { + return QueryExpressionRenderer.withQueryExpression(queryExpressionModel) + .withRenderingContext(renderingContext) + .build() + .render(); + } + + private FragmentAndParameters renderOrderBy(OrderByModel orderByModel) { + return new OrderByRenderer(renderingContext).render(orderByModel); + } + + private FragmentAndParameters renderPagingModel(PagingModel pagingModel) { + return new PagingModelRenderer.Builder() + .withPagingModel(pagingModel) + .withRenderingContext(renderingContext) + .build() + .render(); + } + + public static Builder withSelectModel(SelectModel selectModel) { + return new Builder().withSelectModel(selectModel); + } + + public static class Builder { + private @Nullable SelectModel selectModel; + private @Nullable RenderingContext renderingContext; + private @Nullable String prefix; + private @Nullable String suffix; + + public Builder withRenderingContext(RenderingContext renderingContext) { + this.renderingContext = renderingContext; + return this; + } + + public Builder withSelectModel(SelectModel selectModel) { + this.selectModel = selectModel; + return this; + } + + public Builder withPrefix(String prefix) { + this.prefix = prefix; + return this; + } + + public Builder withSuffix(String suffix) { + this.suffix = suffix; + return this; + } + + public SubQueryRenderer build() { + return new SubQueryRenderer(this); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/TableExpressionRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/TableExpressionRenderer.java new file mode 100644 index 000000000..8114e2411 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/TableExpressionRenderer.java @@ -0,0 +1,64 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.select.render; + +import java.util.Objects; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.SqlTable; +import org.mybatis.dynamic.sql.TableExpressionVisitor; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.select.SubQuery; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + +public class TableExpressionRenderer implements TableExpressionVisitor { + private final RenderingContext renderingContext; + + private TableExpressionRenderer(Builder builder) { + renderingContext = Objects.requireNonNull(builder.renderingContext); + } + + @Override + public FragmentAndParameters visit(SqlTable table) { + return FragmentAndParameters.fromFragment(renderingContext.aliasedTableName(table)); + } + + @Override + public FragmentAndParameters visit(SubQuery subQuery) { + String suffix = subQuery.alias().map(a -> ") " + a) //$NON-NLS-1$ + .orElse(")"); //$NON-NLS-1$ + + return SubQueryRenderer.withSelectModel(subQuery.selectModel()) + .withRenderingContext(renderingContext) + .withPrefix("(")//$NON-NLS-1$ + .withSuffix(suffix) + .build() + .render(); + } + + public static class Builder { + private @Nullable RenderingContext renderingContext; + + public Builder withRenderingContext(RenderingContext renderingContext) { + this.renderingContext = renderingContext; + return this; + } + + public TableExpressionRenderer build() { + return new TableExpressionRenderer(this); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/package-info.java b/src/main/java/org/mybatis/dynamic/sql/select/render/package-info.java new file mode 100644 index 000000000..d2f457254 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +@NullMarked +package org.mybatis.dynamic.sql.select.render; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/mybatis/dynamic/sql/update/MyBatis3UpdateModelAdapter.java b/src/main/java/org/mybatis/dynamic/sql/update/MyBatis3UpdateModelAdapter.java deleted file mode 100644 index e0b91b0ed..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/update/MyBatis3UpdateModelAdapter.java +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright 2016-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.update; - -import java.util.Objects; -import java.util.function.Function; - -import org.mybatis.dynamic.sql.render.RenderingStrategy; -import org.mybatis.dynamic.sql.update.render.UpdateStatementProvider; - -/** - * This adapter will render the underlying update model for MyBatis3, and then call a MyBatis mapper method. - * - * @deprecated in favor of {@link UpdateDSLCompleter}. This class will be removed without direct - * replacement in a future version. - * @author Jeff Butler - * - */ -@Deprecated -public class MyBatis3UpdateModelAdapter { - - private UpdateModel updateModel; - private Function mapperMethod; - - private MyBatis3UpdateModelAdapter(UpdateModel updateModel, Function mapperMethod) { - this.updateModel = Objects.requireNonNull(updateModel); - this.mapperMethod = Objects.requireNonNull(mapperMethod); - } - - public R execute() { - return mapperMethod.apply(updateStatement()); - } - - private UpdateStatementProvider updateStatement() { - return updateModel.render(RenderingStrategy.MYBATIS3); - } - - public static MyBatis3UpdateModelAdapter of(UpdateModel updateModel, - Function mapperMethod) { - return new MyBatis3UpdateModelAdapter<>(updateModel, mapperMethod); - } -} diff --git a/src/main/java/org/mybatis/dynamic/sql/update/UpdateDSL.java b/src/main/java/org/mybatis/dynamic/sql/update/UpdateDSL.java index 7980eb697..fbac97595 100644 --- a/src/main/java/org/mybatis/dynamic/sql/update/UpdateDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/update/UpdateDSL.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -16,110 +16,129 @@ package org.mybatis.dynamic.sql.update; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; -import java.util.function.ToIntFunction; +import org.jspecify.annotations.Nullable; import org.mybatis.dynamic.sql.BasicColumn; -import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.SortSpecification; import org.mybatis.dynamic.sql.SqlColumn; -import org.mybatis.dynamic.sql.SqlCriterion; import org.mybatis.dynamic.sql.SqlTable; -import org.mybatis.dynamic.sql.VisitableCondition; +import org.mybatis.dynamic.sql.common.OrderByModel; +import org.mybatis.dynamic.sql.configuration.StatementConfiguration; import org.mybatis.dynamic.sql.select.SelectModel; -import org.mybatis.dynamic.sql.update.render.UpdateStatementProvider; +import org.mybatis.dynamic.sql.util.AbstractColumnMapping; import org.mybatis.dynamic.sql.util.Buildable; -import org.mybatis.dynamic.sql.util.ColumnMapping; +import org.mybatis.dynamic.sql.util.ColumnToColumnMapping; import org.mybatis.dynamic.sql.util.ConstantMapping; import org.mybatis.dynamic.sql.util.NullMapping; import org.mybatis.dynamic.sql.util.SelectMapping; import org.mybatis.dynamic.sql.util.StringConstantMapping; -import org.mybatis.dynamic.sql.util.UpdateMapping; import org.mybatis.dynamic.sql.util.ValueMapping; -import org.mybatis.dynamic.sql.util.mybatis3.MyBatis3Utils; -import org.mybatis.dynamic.sql.where.AbstractWhereDSL; -import org.mybatis.dynamic.sql.where.WhereApplier; -import org.mybatis.dynamic.sql.where.WhereModel; - -public class UpdateDSL implements Buildable { - - private Function adapterFunction; - private List columnMappings = new ArrayList<>(); - private SqlTable table; - private UpdateWhereBuilder whereBuilder = new UpdateWhereBuilder(); - - private UpdateDSL(SqlTable table, Function adapterFunction) { +import org.mybatis.dynamic.sql.util.ValueOrNullMapping; +import org.mybatis.dynamic.sql.util.ValueWhenPresentMapping; +import org.mybatis.dynamic.sql.where.AbstractWhereFinisher; +import org.mybatis.dynamic.sql.where.AbstractWhereStarter; +import org.mybatis.dynamic.sql.where.EmbeddedWhereModel; + +public class UpdateDSL implements AbstractWhereStarter.UpdateWhereBuilder, UpdateDSL>, + Buildable { + + private final Function adapterFunction; + private final List columnMappings = new ArrayList<>(); + private final SqlTable table; + private final @Nullable String tableAlias; + private @Nullable UpdateWhereBuilder whereBuilder; + private final StatementConfiguration statementConfiguration = new StatementConfiguration(); + private @Nullable Long limit; + private @Nullable OrderByModel orderByModel; + + private UpdateDSL(SqlTable table, @Nullable String tableAlias, Function adapterFunction) { this.table = Objects.requireNonNull(table); + this.tableAlias = tableAlias; this.adapterFunction = Objects.requireNonNull(adapterFunction); } - + public SetClauseFinisher set(SqlColumn column) { return new SetClauseFinisher<>(column); } - + + @Override public UpdateWhereBuilder where() { + whereBuilder = Objects.requireNonNullElseGet(whereBuilder, UpdateWhereBuilder::new); return whereBuilder; } - - public UpdateWhereBuilder where(BindableColumn column, VisitableCondition condition, - SqlCriterion...subCriteria) { - whereBuilder.where(column, condition, subCriteria); - return whereBuilder; + + public UpdateDSL limit(long limit) { + return limitWhenPresent(limit); + } + + public UpdateDSL limitWhenPresent(@Nullable Long limit) { + this.limit = limit; + return this; + } + + public UpdateDSL orderBy(SortSpecification... columns) { + return orderBy(Arrays.asList(columns)); } - public UpdateWhereBuilder applyWhere(WhereApplier whereApplier) { - return whereBuilder.applyWhere(whereApplier); + public UpdateDSL orderBy(Collection columns) { + orderByModel = OrderByModel.of(columns); + return this; } /** * WARNING! Calling this method could result in an update statement that updates * all rows in a table. - * + * * @return the update model */ @Override public R build() { UpdateModel updateModel = UpdateModel.withTable(table) + .withTableAlias(tableAlias) .withColumnMappings(columnMappings) - .withWhereModel(whereBuilder.buildWhereModel()) + .withLimit(limit) + .withOrderByModel(orderByModel) + .withWhereModel(whereBuilder == null ? null : whereBuilder.buildWhereModel()) + .withStatementConfiguration(statementConfiguration) .build(); + return adapterFunction.apply(updateModel); } - - public static UpdateDSL update(Function adapterFunction, SqlTable table) { - return new UpdateDSL<>(table, adapterFunction); + + @Override + public UpdateDSL configureStatement(Consumer consumer) { + consumer.accept(statementConfiguration); + return this; + } + + public static UpdateDSL update(Function adapterFunction, SqlTable table, + @Nullable String tableAlias) { + return new UpdateDSL<>(table, tableAlias, adapterFunction); } - + public static UpdateDSL update(SqlTable table) { - return update(Function.identity(), table); + return update(Function.identity(), table, null); } - - /** - * Executes an update using a MyBatis3 mapper method. - * - * @deprecated in favor of {@link MyBatis3Utils#update(ToIntFunction, SqlTable, UpdateDSLCompleter)}. This - * method will be removed without direct replacement in a future version. - * @param return value from an update method - typically Integer - * @param mapperMethod MyBatis3 mapper method that performs the update - * @param table table to update - * @return number of records updated - typically as Integer - */ - @Deprecated - public static UpdateDSL> updateWithMapper( - Function mapperMethod, SqlTable table) { - return update(updateModel -> MyBatis3UpdateModelAdapter.of(updateModel, mapperMethod), table); + + public static UpdateDSL update(SqlTable table, String tableAlias) { + return update(Function.identity(), table, tableAlias); } - + public class SetClauseFinisher { - - private SqlColumn column; - + + private final SqlColumn column; + public SetClauseFinisher(SqlColumn column) { this.column = column; } - + public UpdateDSL equalToNull() { columnMappings.add(NullMapping.of(column)); return UpdateDSL.this; @@ -129,12 +148,12 @@ public UpdateDSL equalToConstant(String constant) { columnMappings.add(ConstantMapping.of(column, constant)); return UpdateDSL.this; } - + public UpdateDSL equalToStringConstant(String constant) { columnMappings.add(StringConstantMapping.of(column, constant)); return UpdateDSL.this; } - + public UpdateDSL equalTo(T value) { return equalTo(() -> value); } @@ -150,41 +169,64 @@ public UpdateDSL equalTo(Buildable buildable) { } public UpdateDSL equalTo(BasicColumn rightColumn) { - columnMappings.add(ColumnMapping.of(column, rightColumn)); + columnMappings.add(ColumnToColumnMapping.of(column, rightColumn)); return UpdateDSL.this; } - public UpdateDSL equalToWhenPresent(T value) { + public UpdateDSL equalToOrNull(@Nullable T value) { + return equalToOrNull(() -> value); + } + + public UpdateDSL equalToOrNull(Supplier<@Nullable T> valueSupplier) { + columnMappings.add(ValueOrNullMapping.of(column, valueSupplier)); + return UpdateDSL.this; + } + + public UpdateDSL equalToWhenPresent(@Nullable T value) { return equalToWhenPresent(() -> value); } - public UpdateDSL equalToWhenPresent(Supplier valueSupplier) { - if (valueSupplier.get() != null) { - columnMappings.add(ValueMapping.of(column, valueSupplier)); - } + public UpdateDSL equalToWhenPresent(Supplier<@Nullable T> valueSupplier) { + columnMappings.add(ValueWhenPresentMapping.of(column, valueSupplier)); return UpdateDSL.this; } } - public class UpdateWhereBuilder extends AbstractWhereDSL implements Buildable { - - public UpdateWhereBuilder() { - super(); + public class UpdateWhereBuilder extends AbstractWhereFinisher implements Buildable { + + private UpdateWhereBuilder() { + super(UpdateDSL.this); + } + + public UpdateDSL limit(long limit) { + return limitWhenPresent(limit); + } + + public UpdateDSL limitWhenPresent(@Nullable Long limit) { + return UpdateDSL.this.limitWhenPresent(limit); } - + + public UpdateDSL orderBy(SortSpecification... columns) { + return orderBy(Arrays.asList(columns)); + } + + public UpdateDSL orderBy(Collection columns) { + orderByModel = OrderByModel.of(columns); + return UpdateDSL.this; + } + @Override public R build() { return UpdateDSL.this.build(); } - + @Override protected UpdateWhereBuilder getThis() { return this; } - @Override - protected WhereModel buildWhereModel() { - return super.internalBuild(); + protected EmbeddedWhereModel buildWhereModel() { + return buildModel(); } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/update/UpdateDSLCompleter.java b/src/main/java/org/mybatis/dynamic/sql/update/UpdateDSLCompleter.java index 6f4cdd2bf..30512c46a 100644 --- a/src/main/java/org/mybatis/dynamic/sql/update/UpdateDSLCompleter.java +++ b/src/main/java/org/mybatis/dynamic/sql/update/UpdateDSLCompleter.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -26,41 +26,41 @@ * Represents a function that can be used to create a general update method. When using this function, * you can create a method that does not require a user to call the build() and render() methods - making * client code look a bit cleaner. - * + * *

This function is intended to be used in conjunction in the utility method like * {@link MyBatis3Utils#update(ToIntFunction, SqlTable, UpdateDSLCompleter)} - * + * *

For example, you can create mapper interface methods like this: - * + * *

  * @UpdateProvider(type=SqlProviderAdapter.class, method="update")
  * int update(UpdateStatementProvider updateStatement);
- *   
+ *
  * default int update(UpdateDSLCompleter completer) {
         return MyBatis3Utils.update(this::update, person, completer);
  * }
  * 
- * + * *

And then call the simplified default method like this: - * + * *

  * int rows = mapper.update(c ->
  *                c.set(firstName).equalTo("Fred")
  *                .where(id, isEqualTo(100))
  *            );
  * 
- * + * *

You can implement an "update all" simply by omitting a where clause: - * + * *

  * int rows = mapper.update(c ->
  *                c.set(firstName).equalTo("Fred")
  *            );
  * 
- * + * *

You could also implement a helper method that would set fields based on values of a record. For example, - * the following method would set all fields of a record based on whether or not the values are null: - * + * the following method would set all fields of a row based on whether the values are null: + * *

  * static UpdateDSL<UpdateModel> updateSelectiveColumns(PersonRecord record,
  *         UpdateDSL<UpdateModel> dsl) {
@@ -72,18 +72,18 @@
  *             .set(occupation).equalToWhenPresent(record::getOccupation);
  * }
  * 
- * + * *

The helper method could be used like this: - * + * *

  * rows = mapper.update(c ->
  *        PersonMapper.updateSelectiveColumns(record, c)
  *        .where(id, isLessThan(100)));
  * 
- * + * *

In this way, you could mimic the function of the old style "updateByExampleSelective" methods from * MyBatis Generator. - * + * * @author Jeff Butler */ @FunctionalInterface diff --git a/src/main/java/org/mybatis/dynamic/sql/update/UpdateModel.java b/src/main/java/org/mybatis/dynamic/sql/update/UpdateModel.java index d006da8ca..7fd07f766 100644 --- a/src/main/java/org/mybatis/dynamic/sql/update/UpdateModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/update/UpdateModel.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -19,72 +19,92 @@ import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.function.Function; import java.util.stream.Stream; -import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.Nullable; import org.mybatis.dynamic.sql.SqlTable; +import org.mybatis.dynamic.sql.common.CommonBuilder; +import org.mybatis.dynamic.sql.common.OrderByModel; +import org.mybatis.dynamic.sql.configuration.StatementConfiguration; import org.mybatis.dynamic.sql.render.RenderingStrategy; import org.mybatis.dynamic.sql.update.render.UpdateRenderer; import org.mybatis.dynamic.sql.update.render.UpdateStatementProvider; -import org.mybatis.dynamic.sql.util.UpdateMapping; -import org.mybatis.dynamic.sql.where.WhereModel; +import org.mybatis.dynamic.sql.util.AbstractColumnMapping; +import org.mybatis.dynamic.sql.util.Validator; +import org.mybatis.dynamic.sql.where.EmbeddedWhereModel; public class UpdateModel { - private SqlTable table; - private WhereModel whereModel; - private List columnMappings; - + private final SqlTable table; + private final @Nullable String tableAlias; + private final @Nullable EmbeddedWhereModel whereModel; + private final List columnMappings; + private final @Nullable Long limit; + private final @Nullable OrderByModel orderByModel; + private final StatementConfiguration statementConfiguration; + private UpdateModel(Builder builder) { - table = Objects.requireNonNull(builder.table); - whereModel = builder.whereModel; + table = Objects.requireNonNull(builder.table()); + whereModel = builder.whereModel(); columnMappings = Objects.requireNonNull(builder.columnMappings); + tableAlias = builder.tableAlias(); + limit = builder.limit(); + orderByModel = builder.orderByModel(); + Validator.assertNotEmpty(columnMappings, "ERROR.17"); //$NON-NLS-1$ + statementConfiguration = Objects.requireNonNull(builder.statementConfiguration()); } - + public SqlTable table() { return table; } - - public Optional whereModel() { + + public Optional tableAlias() { + return Optional.ofNullable(tableAlias); + } + + public Optional whereModel() { return Optional.ofNullable(whereModel); } - - public Stream mapColumnMappings(Function mapper) { - return columnMappings.stream().map(mapper); + + public Stream columnMappings() { + return columnMappings.stream(); + } + + public Optional limit() { + return Optional.ofNullable(limit); + } + + public Optional orderByModel() { + return Optional.ofNullable(orderByModel); + } + + public StatementConfiguration statementConfiguration() { + return statementConfiguration; } - @NotNull public UpdateStatementProvider render(RenderingStrategy renderingStrategy) { return UpdateRenderer.withUpdateModel(this) .withRenderingStrategy(renderingStrategy) .build() .render(); } - + public static Builder withTable(SqlTable table) { return new Builder().withTable(table); } - - public static class Builder { - private SqlTable table; - private WhereModel whereModel; - private List columnMappings = new ArrayList<>(); - - public Builder withTable(SqlTable table) { - this.table = table; - return this; - } - - public Builder withColumnMappings(List columnMappings) { + + public static class Builder extends CommonBuilder { + private final List columnMappings = new ArrayList<>(); + + public Builder withColumnMappings(List columnMappings) { this.columnMappings.addAll(columnMappings); return this; } - - public Builder withWhereModel(WhereModel whereModel) { - this.whereModel = whereModel; + + @Override + protected Builder getThis() { return this; } - + public UpdateModel build() { return new UpdateModel(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/update/package-info.java b/src/main/java/org/mybatis/dynamic/sql/update/package-info.java new file mode 100644 index 000000000..b1b75a4f4 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/update/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +@NullMarked +package org.mybatis.dynamic.sql.update; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/mybatis/dynamic/sql/update/render/DefaultUpdateStatementProvider.java b/src/main/java/org/mybatis/dynamic/sql/update/render/DefaultUpdateStatementProvider.java index 395436992..eb7f9fba6 100644 --- a/src/main/java/org/mybatis/dynamic/sql/update/render/DefaultUpdateStatementProvider.java +++ b/src/main/java/org/mybatis/dynamic/sql/update/render/DefaultUpdateStatementProvider.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -19,13 +19,15 @@ import java.util.Map; import java.util.Objects; +import org.jspecify.annotations.Nullable; + public class DefaultUpdateStatementProvider implements UpdateStatementProvider { - private String updateStatement; - private Map parameters = new HashMap<>(); + private final String updateStatement; + private final Map parameters; private DefaultUpdateStatementProvider(Builder builder) { updateStatement = Objects.requireNonNull(builder.updateStatement); - parameters.putAll(builder.parameters); + parameters = builder.parameters; } @Override @@ -37,25 +39,25 @@ public Map getParameters() { public String getUpdateStatement() { return updateStatement; } - + public static Builder withUpdateStatement(String updateStatement) { return new Builder().withUpdateStatement(updateStatement); } - + public static class Builder { - private String updateStatement; - private Map parameters = new HashMap<>(); - + private @Nullable String updateStatement; + private final Map parameters = new HashMap<>(); + public Builder withUpdateStatement(String updateStatement) { this.updateStatement = updateStatement; return this; } - + public Builder withParameters(Map parameters) { this.parameters.putAll(parameters); return this; } - + public DefaultUpdateStatementProvider build() { return new DefaultUpdateStatementProvider(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/update/render/SetPhraseVisitor.java b/src/main/java/org/mybatis/dynamic/sql/update/render/SetPhraseVisitor.java index abb3f4d69..5667830e2 100644 --- a/src/main/java/org/mybatis/dynamic/sql/update/render/SetPhraseVisitor.java +++ b/src/main/java/org/mybatis/dynamic/sql/update/render/SetPhraseVisitor.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -16,101 +16,108 @@ package org.mybatis.dynamic.sql.update.render; import java.util.Objects; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; - -import org.mybatis.dynamic.sql.SqlColumn; -import org.mybatis.dynamic.sql.render.RenderingStrategy; -import org.mybatis.dynamic.sql.render.TableAliasCalculator; -import org.mybatis.dynamic.sql.select.render.SelectRenderer; -import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; -import org.mybatis.dynamic.sql.util.ColumnMapping; +import java.util.Optional; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.render.RenderedParameterInfo; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.select.render.SubQueryRenderer; +import org.mybatis.dynamic.sql.util.AbstractColumnMapping; +import org.mybatis.dynamic.sql.util.ColumnToColumnMapping; import org.mybatis.dynamic.sql.util.ConstantMapping; import org.mybatis.dynamic.sql.util.FragmentAndParameters; import org.mybatis.dynamic.sql.util.NullMapping; import org.mybatis.dynamic.sql.util.SelectMapping; import org.mybatis.dynamic.sql.util.StringConstantMapping; +import org.mybatis.dynamic.sql.util.StringUtilities; import org.mybatis.dynamic.sql.util.UpdateMappingVisitor; import org.mybatis.dynamic.sql.util.ValueMapping; +import org.mybatis.dynamic.sql.util.ValueOrNullMapping; +import org.mybatis.dynamic.sql.util.ValueWhenPresentMapping; + +public class SetPhraseVisitor extends UpdateMappingVisitor> { + + private final RenderingContext renderingContext; -public class SetPhraseVisitor implements UpdateMappingVisitor { - - private AtomicInteger sequence; - private RenderingStrategy renderingStrategy; - - public SetPhraseVisitor(AtomicInteger sequence, RenderingStrategy renderingStrategy) { - this.sequence = Objects.requireNonNull(sequence); - this.renderingStrategy = Objects.requireNonNull(renderingStrategy); + public SetPhraseVisitor(RenderingContext renderingContext) { + this.renderingContext = Objects.requireNonNull(renderingContext); } @Override - public FragmentAndParameters visit(NullMapping mapping) { - return FragmentAndParameters.withFragment(mapping.mapColumn(SqlColumn::name) + " = null") //$NON-NLS-1$ - .build(); + public Optional visit(NullMapping mapping) { + return buildNullFragment(mapping); } @Override - public FragmentAndParameters visit(ConstantMapping mapping) { - String fragment = mapping.mapColumn(SqlColumn::name) + " = " + mapping.constant(); //$NON-NLS-1$ + public Optional visit(ConstantMapping mapping) { + String fragment = renderingContext.aliasedColumnName(mapping.column()) + + " = " + mapping.constant(); //$NON-NLS-1$ return FragmentAndParameters.withFragment(fragment) - .build(); + .buildOptional(); } @Override - public FragmentAndParameters visit(StringConstantMapping mapping) { - String fragment = mapping.mapColumn(SqlColumn::name) - + " = '" //$NON-NLS-1$ - + mapping.constant() - + "'"; //$NON-NLS-1$ - + public Optional visit(StringConstantMapping mapping) { + String fragment = renderingContext.aliasedColumnName(mapping.column()) + + " = " //$NON-NLS-1$ + + StringUtilities.formatConstantForSQL(mapping.constant()); + return FragmentAndParameters.withFragment(fragment) - .build(); + .buildOptional(); } - + @Override - public FragmentAndParameters visit(ValueMapping mapping) { - String mapKey = RenderingStrategy.formatParameterMapKey(sequence); + public Optional visit(ValueMapping mapping) { + return buildValueFragment(mapping, mapping.value()); + } - String jdbcPlaceholder = mapping.mapColumn(toJdbcPlaceholder(mapKey)); - String setPhrase = mapping.mapColumn(SqlColumn::name) - + " = " //$NON-NLS-1$ - + jdbcPlaceholder; - - return FragmentAndParameters.withFragment(setPhrase) - .withParameter(mapKey, mapping.value()) - .build(); + @Override + public Optional visit(ValueOrNullMapping mapping) { + return mapping.value() + .map(v -> buildValueFragment(mapping, v)) + .orElseGet(() -> buildNullFragment(mapping)); } @Override - public FragmentAndParameters visit(SelectMapping mapping) { - SelectStatementProvider selectStatement = SelectRenderer.withSelectModel(mapping.selectModel()) - .withRenderingStrategy(renderingStrategy) - .withSequence(sequence) + public Optional visit(ValueWhenPresentMapping mapping) { + return mapping.value().flatMap(v -> buildValueFragment(mapping, v)); + } + + @Override + public Optional visit(SelectMapping mapping) { + String prefix = renderingContext.aliasedColumnName(mapping.column()) + " = ("; //$NON-NLS-1$ + + FragmentAndParameters fragmentAndParameters = SubQueryRenderer.withSelectModel(mapping.selectModel()) + .withRenderingContext(renderingContext) + .withPrefix(prefix) + .withSuffix(")") //$NON-NLS-1$ .build() .render(); - - String fragment = mapping.mapColumn(SqlColumn::name) - + " = (" //$NON-NLS-1$ - + selectStatement.getSelectStatement() - + ")"; //$NON-NLS-1$ - - return FragmentAndParameters.withFragment(fragment) - .withParameters(selectStatement.getParameters()) - .build(); + + return Optional.of(fragmentAndParameters); } @Override - public FragmentAndParameters visit(ColumnMapping mapping) { - String setPhrase = mapping.mapColumn(SqlColumn::name) + public Optional visit(ColumnToColumnMapping mapping) { + FragmentAndParameters fragmentAndParameters = mapping.rightColumn().render(renderingContext) + .mapFragment(f -> renderingContext.aliasedColumnName(mapping.column()) + " = " + f); //$NON-NLS-1$ + return Optional.of(fragmentAndParameters); + } + + private Optional buildValueFragment(AbstractColumnMapping mapping, @Nullable T value) { + RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo(mapping.column()); + String setPhrase = renderingContext.aliasedColumnName(mapping.column()) + " = " //$NON-NLS-1$ - + mapping.rightColumn().renderWithTableAlias(TableAliasCalculator.empty()); - + + parameterInfo.renderedPlaceHolder(); + return FragmentAndParameters.withFragment(setPhrase) - .build(); + .withParameter(parameterInfo.parameterMapKey(), value) + .buildOptional(); } - - private Function, String> toJdbcPlaceholder(String parameterName) { - return column -> renderingStrategy - .getFormattedJdbcPlaceholder(column, RenderingStrategy.DEFAULT_PARAMETER_PREFIX, parameterName); + + private Optional buildNullFragment(AbstractColumnMapping mapping) { + return FragmentAndParameters + .withFragment(renderingContext.aliasedColumnName(mapping.column()) + " = null") //$NON-NLS-1$ + .buildOptional(); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/update/render/UpdateRenderer.java b/src/main/java/org/mybatis/dynamic/sql/update/render/UpdateRenderer.java index 2bf75bb90..ee0f74961 100644 --- a/src/main/java/org/mybatis/dynamic/sql/update/render/UpdateRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/update/render/UpdateRenderer.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,115 +15,124 @@ */ package org.mybatis.dynamic.sql.update.render; -import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore; - import java.util.Objects; import java.util.Optional; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.common.OrderByModel; +import org.mybatis.dynamic.sql.common.OrderByRenderer; +import org.mybatis.dynamic.sql.render.ExplicitTableAliasCalculator; +import org.mybatis.dynamic.sql.render.RenderedParameterInfo; +import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.render.RenderingStrategy; import org.mybatis.dynamic.sql.render.TableAliasCalculator; import org.mybatis.dynamic.sql.update.UpdateModel; import org.mybatis.dynamic.sql.util.FragmentAndParameters; import org.mybatis.dynamic.sql.util.FragmentCollector; -import org.mybatis.dynamic.sql.util.UpdateMapping; -import org.mybatis.dynamic.sql.where.WhereModel; -import org.mybatis.dynamic.sql.where.render.WhereClauseProvider; -import org.mybatis.dynamic.sql.where.render.WhereRenderer; +import org.mybatis.dynamic.sql.util.Validator; +import org.mybatis.dynamic.sql.where.EmbeddedWhereModel; public class UpdateRenderer { - private UpdateModel updateModel; - private RenderingStrategy renderingStrategy; - private AtomicInteger sequence = new AtomicInteger(1); - + private final UpdateModel updateModel; + private final RenderingContext renderingContext; + private final SetPhraseVisitor visitor; + private UpdateRenderer(Builder builder) { updateModel = Objects.requireNonNull(builder.updateModel); - renderingStrategy = Objects.requireNonNull(builder.renderingStrategy); + TableAliasCalculator tableAliasCalculator = builder.updateModel.tableAlias() + .map(a -> ExplicitTableAliasCalculator.of(updateModel.table(), a)) + .orElseGet(TableAliasCalculator::empty); + renderingContext = RenderingContext + .withRenderingStrategy(Objects.requireNonNull(builder.renderingStrategy)) + .withTableAliasCalculator(tableAliasCalculator) + .withStatementConfiguration(updateModel.statementConfiguration()) + .build(); + visitor = new SetPhraseVisitor(renderingContext); } - + public UpdateStatementProvider render() { - FragmentCollector fc = calculateColumnMappings(); - - return updateModel.whereModel() - .flatMap(this::renderWhereClause) - .map(wc -> renderWithWhereClause(fc, wc)) - .orElseGet(() -> renderWithoutWhereClause(fc)); - } + FragmentCollector fragmentCollector = new FragmentCollector(); - private FragmentCollector calculateColumnMappings() { - SetPhraseVisitor visitor = new SetPhraseVisitor(sequence, renderingStrategy); + fragmentCollector.add(calculateUpdateStatementStart()); + fragmentCollector.add(calculateSetPhrase()); + calculateWhereClause().ifPresent(fragmentCollector::add); + calculateOrderByClause().ifPresent(fragmentCollector::add); + calculateLimitClause().ifPresent(fragmentCollector::add); - return updateModel.mapColumnMappings(toFragmentAndParameters(visitor)) - .collect(FragmentCollector.collect()); + return toUpdateStatementProvider(fragmentCollector); } - - private UpdateStatementProvider renderWithWhereClause(FragmentCollector columnMappings, - WhereClauseProvider whereClause) { - return DefaultUpdateStatementProvider.withUpdateStatement(calculateUpdateStatement(columnMappings, whereClause)) - .withParameters(columnMappings.parameters()) - .withParameters(whereClause.getParameters()) + + private UpdateStatementProvider toUpdateStatementProvider(FragmentCollector fragmentCollector) { + return DefaultUpdateStatementProvider + .withUpdateStatement(fragmentCollector.collectFragments(Collectors.joining(" "))) //$NON-NLS-1$ + .withParameters(fragmentCollector.parameters()) .build(); } - private String calculateUpdateStatement(FragmentCollector fc, WhereClauseProvider whereClause) { - return calculateUpdateStatement(fc) - + spaceBefore(whereClause.getWhereClause()); + private FragmentAndParameters calculateUpdateStatementStart() { + String aliasedTableName = renderingContext.aliasedTableName(updateModel.table()); + return FragmentAndParameters.fromFragment("update " + aliasedTableName); //$NON-NLS-1$ } - - private String calculateUpdateStatement(FragmentCollector fc) { - return "update" //$NON-NLS-1$ - + spaceBefore(updateModel.table().tableNameAtRuntime()) - + spaceBefore(calculateSetPhrase(fc)); + + private FragmentAndParameters calculateSetPhrase() { + FragmentCollector fragmentCollector = updateModel.columnMappings() + .map(m -> m.accept(visitor)) + .flatMap(Optional::stream) + .collect(FragmentCollector.collect()); + + Validator.assertFalse(fragmentCollector.isEmpty(), "ERROR.18"); //$NON-NLS-1$ + + return fragmentCollector.toFragmentAndParameters( + Collectors.joining(", ", "set ", "")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } - - private UpdateStatementProvider renderWithoutWhereClause(FragmentCollector columnMappings) { - return DefaultUpdateStatementProvider.withUpdateStatement(calculateUpdateStatement(columnMappings)) - .withParameters(columnMappings.parameters()) - .build(); + + private Optional calculateWhereClause() { + return updateModel.whereModel().flatMap(this::renderWhereClause); } - private String calculateSetPhrase(FragmentCollector collector) { - return collector.fragments() - .collect(Collectors.joining(", ", "set ", "")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + private Optional renderWhereClause(EmbeddedWhereModel whereModel) { + return whereModel.render(renderingContext); } - - private Optional renderWhereClause(WhereModel whereModel) { - return WhereRenderer.withWhereModel(whereModel) - .withRenderingStrategy(renderingStrategy) - .withSequence(sequence) - .withTableAliasCalculator(TableAliasCalculator.empty()) - .build() - .render(); + + private Optional calculateLimitClause() { + return updateModel.limit().map(this::renderLimitClause); } - private Function toFragmentAndParameters(SetPhraseVisitor visitor) { - return updateMapping -> toFragmentAndParameters(visitor, updateMapping); + private FragmentAndParameters renderLimitClause(Long limit) { + RenderedParameterInfo parameterInfo = renderingContext.calculateLimitParameterInfo(); + + return FragmentAndParameters.withFragment("limit " + parameterInfo.renderedPlaceHolder()) //$NON-NLS-1$ + .withParameter(parameterInfo.parameterMapKey(), limit) + .build(); } - - private FragmentAndParameters toFragmentAndParameters(SetPhraseVisitor visitor, UpdateMapping updateMapping) { - return updateMapping.accept(visitor); + + private Optional calculateOrderByClause() { + return updateModel.orderByModel().map(this::renderOrderByClause); } - + + private FragmentAndParameters renderOrderByClause(OrderByModel orderByModel) { + return new OrderByRenderer(renderingContext).render(orderByModel); + } + public static Builder withUpdateModel(UpdateModel updateModel) { return new Builder().withUpdateModel(updateModel); } - + public static class Builder { - private UpdateModel updateModel; - private RenderingStrategy renderingStrategy; + private @Nullable UpdateModel updateModel; + private @Nullable RenderingStrategy renderingStrategy; public Builder withUpdateModel(UpdateModel updateModel) { this.updateModel = updateModel; return this; } - + public Builder withRenderingStrategy(RenderingStrategy renderingStrategy) { this.renderingStrategy = renderingStrategy; return this; } - + public UpdateRenderer build() { return new UpdateRenderer(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/update/render/UpdateStatementProvider.java b/src/main/java/org/mybatis/dynamic/sql/update/render/UpdateStatementProvider.java index 1d33e8610..087741ae2 100644 --- a/src/main/java/org/mybatis/dynamic/sql/update/render/UpdateStatementProvider.java +++ b/src/main/java/org/mybatis/dynamic/sql/update/render/UpdateStatementProvider.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/mybatis/dynamic/sql/update/render/package-info.java b/src/main/java/org/mybatis/dynamic/sql/update/render/package-info.java new file mode 100644 index 000000000..625393ac4 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/update/render/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +@NullMarked +package org.mybatis.dynamic.sql.update.render; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/mybatis/dynamic/sql/util/AbstractColumnMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/AbstractColumnMapping.java index 1714e9d7e..4107e9590 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/AbstractColumnMapping.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/AbstractColumnMapping.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -16,19 +16,24 @@ package org.mybatis.dynamic.sql.util; import java.util.Objects; -import java.util.function.Function; import org.mybatis.dynamic.sql.SqlColumn; public abstract class AbstractColumnMapping { - protected SqlColumn column; - + protected final SqlColumn column; + protected AbstractColumnMapping(SqlColumn column) { this.column = Objects.requireNonNull(column); } - - public R mapColumn(Function, R> mapper) { - return mapper.apply(column); + + public String columnName() { + return column.name(); } + + @SuppressWarnings("java:S1452") + public SqlColumn column() { + return column; + } + + public abstract R accept(ColumnMappingVisitor visitor); } - \ No newline at end of file diff --git a/src/main/java/org/mybatis/dynamic/sql/util/Buildable.java b/src/main/java/org/mybatis/dynamic/sql/util/Buildable.java index 2d91d46e7..1dcbe70ea 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/Buildable.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/Buildable.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -18,4 +18,4 @@ @FunctionalInterface public interface Buildable { T build(); -} \ No newline at end of file +} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/ColumnMappingVisitor.java b/src/main/java/org/mybatis/dynamic/sql/util/ColumnMappingVisitor.java new file mode 100644 index 000000000..34516105b --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/ColumnMappingVisitor.java @@ -0,0 +1,59 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util; + +/** + * Visitor for all column mappings. Various column mappings are used by insert and update + * statements. Only the null and constant mappings are supported by all statements. Other mappings + * may or may not be supported. For example, it makes no sense to map a column to another column in + * an insert - so the ColumnToColumnMapping is only supported on update statements. + * + *

Rather than implement this interface directly, we recommend extending one of the derived + * classes. The derived classes encapsulate the rules about which mappings are applicable to the + * different types of statements. + * + * @author Jeff Butler + * + * @param + * The type of object created by the visitor + */ +public interface ColumnMappingVisitor { + R visit(NullMapping mapping); + + R visit(ConstantMapping mapping); + + R visit(StringConstantMapping mapping); + + R visit(ValueMapping mapping); + + R visit(ValueOrNullMapping mapping); + + R visit(ValueWhenPresentMapping mapping); + + R visit(SelectMapping mapping); + + R visit(PropertyMapping mapping); + + R visit(PropertyWhenPresentMapping mapping); + + R visit(ColumnToColumnMapping mapping); + + R visit(RowMapping mapping); + + R visit(MappedColumnMapping mapping); + + R visit(MappedColumnWhenPresentMapping mapping); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/ColumnMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/ColumnToColumnMapping.java similarity index 62% rename from src/main/java/org/mybatis/dynamic/sql/util/ColumnMapping.java rename to src/main/java/org/mybatis/dynamic/sql/util/ColumnToColumnMapping.java index fbe668c7d..b287ce215 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/ColumnMapping.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/ColumnToColumnMapping.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -18,25 +18,25 @@ import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.SqlColumn; -public class ColumnMapping extends AbstractColumnMapping implements UpdateMapping { +public class ColumnToColumnMapping extends AbstractColumnMapping { - private BasicColumn rightColumn; - - private ColumnMapping(SqlColumn column, BasicColumn rightColumn) { + private final BasicColumn rightColumn; + + private ColumnToColumnMapping(SqlColumn column, BasicColumn rightColumn) { super(column); this.rightColumn = rightColumn; } - + public BasicColumn rightColumn() { return rightColumn; } - + @Override - public R accept(UpdateMappingVisitor visitor) { + public R accept(ColumnMappingVisitor visitor) { return visitor.visit(this); } - public static ColumnMapping of(SqlColumn column, BasicColumn rightColumn) { - return new ColumnMapping(column, rightColumn); + public static ColumnToColumnMapping of(SqlColumn column, BasicColumn rightColumn) { + return new ColumnToColumnMapping(column, rightColumn); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/util/UpdateMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/ConfigurableStatement.java similarity index 63% rename from src/main/java/org/mybatis/dynamic/sql/util/UpdateMapping.java rename to src/main/java/org/mybatis/dynamic/sql/util/ConfigurableStatement.java index 0bd35ed66..9fedc85a0 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/UpdateMapping.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/ConfigurableStatement.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,12 +15,10 @@ */ package org.mybatis.dynamic.sql.util; -import java.util.function.Function; +import java.util.function.Consumer; -import org.mybatis.dynamic.sql.SqlColumn; +import org.mybatis.dynamic.sql.configuration.StatementConfiguration; -public interface UpdateMapping { - R mapColumn(Function, R> mapper); - - R accept(UpdateMappingVisitor visitor); +public interface ConfigurableStatement { + R configureStatement(Consumer consumer); } diff --git a/src/main/java/org/mybatis/dynamic/sql/util/ConstantMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/ConstantMapping.java index 6e4b89f4c..5396e3499 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/ConstantMapping.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/ConstantMapping.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -18,17 +18,17 @@ import org.mybatis.dynamic.sql.SqlColumn; /** - * This class represents a mapping between a column and a constant. The constant should be rendered - * exactly as specified here. - * - * @author Jeff Butler + * This class represents a mapping between a column and a constant. The constant should be rendered exactly as specified + * here. * + * @author Jeff Butler */ -public class ConstantMapping extends AbstractColumnMapping implements InsertMapping, UpdateMapping { - private String constant; +public class ConstantMapping extends AbstractColumnMapping { + private final String constant; - private ConstantMapping(SqlColumn column) { + private ConstantMapping(SqlColumn column, String constant) { super(column); + this.constant = constant; } public String constant() { @@ -36,18 +36,11 @@ public String constant() { } public static ConstantMapping of(SqlColumn column, String constant) { - ConstantMapping mapping = new ConstantMapping(column); - mapping.constant = constant; - return mapping; - } - - @Override - public R accept(UpdateMappingVisitor visitor) { - return visitor.visit(this); + return new ConstantMapping(column, constant); } @Override - public R accept(InsertMappingVisitor visitor) { + public R accept(ColumnMappingVisitor visitor) { return visitor.visit(this); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/util/CustomCollectors.java b/src/main/java/org/mybatis/dynamic/sql/util/CustomCollectors.java deleted file mode 100644 index 08a490dc6..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/util/CustomCollectors.java +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright 2016-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.util; - -import java.util.StringJoiner; -import java.util.stream.Collector; - -public interface CustomCollectors { - - /** - * Returns a {@code Collector} similar to the standard JDK joining collector, except that - * this collector returns an empty string if there are no elements to collect. - * - * @param delimiter the delimiter to be used between each element - * @param prefix the sequence of characters to be used at the beginning - * of the joined result - * @param suffix the sequence of characters to be used at the end - * of the joined result - * @return A {@code Collector} which concatenates CharSequence elements, - * separated by the specified delimiter, in encounter order - */ - static Collector joining(CharSequence delimiter, CharSequence prefix, - CharSequence suffix) { - return Collector.of(() -> { - StringJoiner sj = new StringJoiner(delimiter, prefix, suffix); - sj.setEmptyValue(""); //$NON-NLS-1$ - return sj; - }, StringJoiner::add, StringJoiner::merge, StringJoiner::toString); - } -} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/FragmentAndParameters.java b/src/main/java/org/mybatis/dynamic/sql/util/FragmentAndParameters.java index f6ff752e1..3426accf8 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/FragmentAndParameters.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/FragmentAndParameters.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,62 +15,79 @@ */ package org.mybatis.dynamic.sql.util; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.function.UnaryOperator; + +import org.jspecify.annotations.Nullable; public class FragmentAndParameters { - - private String fragment; - private Map parameters; - + + private final String fragment; + private final Map parameters; + private FragmentAndParameters(Builder builder) { fragment = Objects.requireNonNull(builder.fragment); - parameters = Objects.requireNonNull(builder.parameters); + parameters = Collections.unmodifiableMap(builder.parameters); } - + public String fragment() { return fragment; } - + public Map parameters() { return parameters; } - - public FragmentAndParameters prependFragment(String s) { - return FragmentAndParameters.withFragment(s + " " + fragment) //$NON-NLS-1$ + + /** + * Return a new instance with the same parameters and a transformed fragment. + * + * @param mapper a function that can change the value of the fragment + * @return a new instance with the same parameters and a transformed fragment + */ + public FragmentAndParameters mapFragment(UnaryOperator mapper) { + return withFragment(mapper.apply(fragment)) .withParameters(parameters) .build(); } - + public static Builder withFragment(String fragment) { return new Builder().withFragment(fragment); } - + + public static FragmentAndParameters fromFragment(String fragment) { + return new Builder().withFragment(fragment).build(); + } + public static class Builder { - private String fragment; - private Map parameters = new HashMap<>(); - + private @Nullable String fragment; + private final Map parameters = new HashMap<>(); + public Builder withFragment(String fragment) { this.fragment = fragment; return this; } - - public Builder withParameter(String key, Object value) { + + public Builder withParameter(String key, @Nullable Object value) { + // the value can be null because a parameter type converter may return null + + //noinspection DataFlowIssue parameters.put(key, value); return this; } - + public Builder withParameters(Map parameters) { this.parameters.putAll(parameters); return this; } - + public FragmentAndParameters build() { return new FragmentAndParameters(this); } - + public Optional buildOptional() { return Optional.of(build()); } diff --git a/src/main/java/org/mybatis/dynamic/sql/util/FragmentCollector.java b/src/main/java/org/mybatis/dynamic/sql/util/FragmentCollector.java index 748091610..410d7a035 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/FragmentCollector.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/FragmentCollector.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -16,51 +16,62 @@ package org.mybatis.dynamic.sql.util; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collector; -import java.util.stream.Stream; public class FragmentCollector { - List fragments = new ArrayList<>(); - Map parameters = new HashMap<>(); - - FragmentCollector() { + final List fragments = new ArrayList<>(); + final Map parameters = new HashMap<>(); + + public FragmentCollector() { super(); } - + private FragmentCollector(FragmentAndParameters initialFragment) { add(initialFragment); } - + public void add(FragmentAndParameters fragmentAndParameters) { fragments.add(fragmentAndParameters.fragment()); parameters.putAll(fragmentAndParameters.parameters()); } - + public FragmentCollector merge(FragmentCollector other) { fragments.addAll(other.fragments); parameters.putAll(other.parameters); return this; } - - public Stream fragments() { - return fragments.stream(); + + public Optional firstFragment() { + return fragments.stream().findFirst(); + } + + public String collectFragments(Collector fragmentCollector) { + return fragments.stream().collect(fragmentCollector); } - + + public FragmentAndParameters toFragmentAndParameters(Collector fragmentCollector) { + return FragmentAndParameters.withFragment(collectFragments(fragmentCollector)) + .withParameters(parameters()) + .build(); + } + public Map parameters() { - return parameters; + return Collections.unmodifiableMap(parameters); } - + public boolean hasMultipleFragments() { return fragments.size() > 1; } - + public boolean isEmpty() { return fragments.isEmpty(); } - + public static Collector collect() { return Collector.of(FragmentCollector::new, FragmentCollector::add, diff --git a/src/main/java/org/mybatis/dynamic/sql/util/GeneralInsertMappingVisitor.java b/src/main/java/org/mybatis/dynamic/sql/util/GeneralInsertMappingVisitor.java new file mode 100644 index 000000000..21303d49b --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/GeneralInsertMappingVisitor.java @@ -0,0 +1,53 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util; + +public abstract class GeneralInsertMappingVisitor implements ColumnMappingVisitor { + @Override + public final R visit(SelectMapping mapping) { + throw new UnsupportedOperationException(Messages.getInternalErrorString(InternalError.INTERNAL_ERROR_1)); + } + + @Override + public final R visit(PropertyMapping mapping) { + throw new UnsupportedOperationException(Messages.getInternalErrorString(InternalError.INTERNAL_ERROR_2)); + } + + @Override + public final R visit(PropertyWhenPresentMapping mapping) { + throw new UnsupportedOperationException(Messages.getInternalErrorString(InternalError.INTERNAL_ERROR_3)); + } + + @Override + public final R visit(ColumnToColumnMapping columnMapping) { + throw new UnsupportedOperationException(Messages.getInternalErrorString(InternalError.INTERNAL_ERROR_4)); + } + + @Override + public final R visit(RowMapping mapping) { + throw new UnsupportedOperationException(Messages.getInternalErrorString(InternalError.INTERNAL_ERROR_14)); + } + + @Override + public R visit(MappedColumnMapping mapping) { + throw new UnsupportedOperationException(Messages.getInternalErrorString(InternalError.INTERNAL_ERROR_16)); + } + + @Override + public R visit(MappedColumnWhenPresentMapping mapping) { + throw new UnsupportedOperationException(Messages.getInternalErrorString(InternalError.INTERNAL_ERROR_17)); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/InsertMappingVisitor.java b/src/main/java/org/mybatis/dynamic/sql/util/InsertMappingVisitor.java index d0d919a98..60770ec15 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/InsertMappingVisitor.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/InsertMappingVisitor.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,12 +15,29 @@ */ package org.mybatis.dynamic.sql.util; -public interface InsertMappingVisitor { - T visit(NullMapping mapping); +public abstract class InsertMappingVisitor implements ColumnMappingVisitor { + @Override + public final R visit(ValueMapping mapping) { + throw new UnsupportedOperationException(Messages.getInternalErrorString(InternalError.INTERNAL_ERROR_5)); + } - T visit(ConstantMapping mapping); + @Override + public final R visit(ValueOrNullMapping mapping) { + throw new UnsupportedOperationException(Messages.getInternalErrorString(InternalError.INTERNAL_ERROR_6)); + } - T visit(StringConstantMapping mapping); + @Override + public final R visit(ValueWhenPresentMapping mapping) { + throw new UnsupportedOperationException(Messages.getInternalErrorString(InternalError.INTERNAL_ERROR_7)); + } - T visit(PropertyMapping mapping); + @Override + public final R visit(SelectMapping mapping) { + throw new UnsupportedOperationException(Messages.getInternalErrorString(InternalError.INTERNAL_ERROR_8)); + } + + @Override + public final R visit(ColumnToColumnMapping columnMapping) { + throw new UnsupportedOperationException(Messages.getInternalErrorString(InternalError.INTERNAL_ERROR_9)); + } } diff --git a/src/main/java/org/mybatis/dynamic/sql/util/InternalError.java b/src/main/java/org/mybatis/dynamic/sql/util/InternalError.java new file mode 100644 index 000000000..e973dad55 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/InternalError.java @@ -0,0 +1,52 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util; + +/** + * Enum for managing internal error numbers. + */ +public enum InternalError { + INTERNAL_ERROR_1(1), + INTERNAL_ERROR_2(2), + INTERNAL_ERROR_3(3), + INTERNAL_ERROR_4(4), + INTERNAL_ERROR_5(5), + INTERNAL_ERROR_6(6), + INTERNAL_ERROR_7(7), + INTERNAL_ERROR_8(8), + INTERNAL_ERROR_9(9), + INTERNAL_ERROR_10(10), + INTERNAL_ERROR_11(11), + INTERNAL_ERROR_12(12), + INTERNAL_ERROR_13(13), + INTERNAL_ERROR_14(14), + INTERNAL_ERROR_15(15), + INTERNAL_ERROR_16(16), + INTERNAL_ERROR_17(17), + INTERNAL_ERROR_18(18), + INTERNAL_ERROR_19(19), + INTERNAL_ERROR_20(20); + + private final int number; + + InternalError(int number) { + this.number = number; + } + + public int getNumber() { + return number; + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/MappedColumnMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/MappedColumnMapping.java new file mode 100644 index 000000000..bef0a57e0 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/MappedColumnMapping.java @@ -0,0 +1,34 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util; + +import org.mybatis.dynamic.sql.SqlColumn; + +public class MappedColumnMapping extends AbstractColumnMapping { + + protected MappedColumnMapping(SqlColumn column) { + super(column); + } + + @Override + public R accept(ColumnMappingVisitor visitor) { + return visitor.visit(this); + } + + public static MappedColumnMapping of(SqlColumn column) { + return new MappedColumnMapping(column); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/MappedColumnWhenPresentMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/MappedColumnWhenPresentMapping.java new file mode 100644 index 000000000..268cae396 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/MappedColumnWhenPresentMapping.java @@ -0,0 +1,43 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util; + +import java.util.Objects; +import java.util.function.Supplier; + +import org.mybatis.dynamic.sql.SqlColumn; + +public class MappedColumnWhenPresentMapping extends MappedColumnMapping { + private final Supplier valueSupplier; + + private MappedColumnWhenPresentMapping(SqlColumn column, Supplier valueSupplier) { + super(column); + this.valueSupplier = Objects.requireNonNull(valueSupplier); + } + + public boolean shouldRender() { + return valueSupplier.get() != null; + } + + @Override + public R accept(ColumnMappingVisitor visitor) { + return visitor.visit(this); + } + + public static MappedColumnWhenPresentMapping of(SqlColumn column, Supplier valueSupplier) { + return new MappedColumnWhenPresentMapping(column, valueSupplier); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/Messages.java b/src/main/java/org/mybatis/dynamic/sql/util/Messages.java new file mode 100644 index 000000000..649dd7747 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/Messages.java @@ -0,0 +1,43 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util; + +import java.text.MessageFormat; +import java.util.ResourceBundle; + +public class Messages { + private static final String BUNDLE_NAME = "org.mybatis.dynamic.sql.util.messages"; //$NON-NLS-1$ + + private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle(BUNDLE_NAME); + + private Messages() {} + + public static String getString(String key) { + return RESOURCE_BUNDLE.getString(key); + } + + public static String getString(String key, String p1) { + return MessageFormat.format(getString(key), p1); + } + + public static String getString(String key, String p1, String p2, String p3) { + return MessageFormat.format(getString(key), p1, p2, p3); + } + + public static String getInternalErrorString(InternalError internalError) { + return MessageFormat.format(getString("INTERNAL.ERROR"), internalError.getNumber()); //$NON-NLS-1$ + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/MultiRowInsertMappingVisitor.java b/src/main/java/org/mybatis/dynamic/sql/util/MultiRowInsertMappingVisitor.java new file mode 100644 index 000000000..668d064e7 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/MultiRowInsertMappingVisitor.java @@ -0,0 +1,28 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util; + +public abstract class MultiRowInsertMappingVisitor extends InsertMappingVisitor { + @Override + public final R visit(PropertyWhenPresentMapping mapping) { + throw new UnsupportedOperationException(Messages.getInternalErrorString(InternalError.INTERNAL_ERROR_12)); + } + + @Override + public R visit(MappedColumnWhenPresentMapping mapping) { + throw new UnsupportedOperationException(Messages.getInternalErrorString(InternalError.INTERNAL_ERROR_18)); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/NullMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/NullMapping.java index 87426396d..ccd95d52f 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/NullMapping.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/NullMapping.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -17,22 +17,17 @@ import org.mybatis.dynamic.sql.SqlColumn; -public class NullMapping extends AbstractColumnMapping implements InsertMapping, UpdateMapping { +public class NullMapping extends AbstractColumnMapping { private NullMapping(SqlColumn column) { super(column); } - + public static NullMapping of(SqlColumn column) { return new NullMapping(column); } @Override - public R accept(UpdateMappingVisitor visitor) { - return visitor.visit(this); - } - - @Override - public R accept(InsertMappingVisitor visitor) { + public R accept(ColumnMappingVisitor visitor) { return visitor.visit(this); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/util/Predicates.java b/src/main/java/org/mybatis/dynamic/sql/util/Predicates.java index 489011503..4fb7efa99 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/Predicates.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/Predicates.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -17,10 +17,12 @@ import java.util.function.BiPredicate; +import org.jspecify.annotations.Nullable; + public class Predicates { private Predicates() {} - - public static BiPredicate bothPresent() { + + public static BiPredicate<@Nullable T, @Nullable T> bothPresent() { return (v1, v2) -> v1 != null && v2 != null; } } diff --git a/src/main/java/org/mybatis/dynamic/sql/util/PropertyMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/PropertyMapping.java index a97f35274..87023c7e9 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/PropertyMapping.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/PropertyMapping.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,27 +15,28 @@ */ package org.mybatis.dynamic.sql.util; +import java.util.Objects; + import org.mybatis.dynamic.sql.SqlColumn; -public class PropertyMapping extends AbstractColumnMapping implements InsertMapping { - private String property; - - private PropertyMapping(SqlColumn column) { +public class PropertyMapping extends AbstractColumnMapping { + private final String property; + + protected PropertyMapping(SqlColumn column, String property) { super(column); + this.property = Objects.requireNonNull(property); } - + public String property() { return property; } @Override - public S accept(InsertMappingVisitor visitor) { + public R accept(ColumnMappingVisitor visitor) { return visitor.visit(this); } - + public static PropertyMapping of(SqlColumn column, String property) { - PropertyMapping mapping = new PropertyMapping(column); - mapping.property = property; - return mapping; + return new PropertyMapping(column, property); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/util/PropertyWhenPresentMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/PropertyWhenPresentMapping.java new file mode 100644 index 000000000..e5f6f3e62 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/PropertyWhenPresentMapping.java @@ -0,0 +1,43 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util; + +import java.util.Objects; +import java.util.function.Supplier; + +import org.mybatis.dynamic.sql.SqlColumn; + +public class PropertyWhenPresentMapping extends PropertyMapping { + private final Supplier valueSupplier; + + private PropertyWhenPresentMapping(SqlColumn column, String property, Supplier valueSupplier) { + super(column, property); + this.valueSupplier = Objects.requireNonNull(valueSupplier); + } + + public boolean shouldRender() { + return valueSupplier.get() != null; + } + + @Override + public R accept(ColumnMappingVisitor visitor) { + return visitor.visit(this); + } + + public static PropertyWhenPresentMapping of(SqlColumn column, String property, Supplier valueSupplier) { + return new PropertyWhenPresentMapping(column, property, valueSupplier); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/RowMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/RowMapping.java new file mode 100644 index 000000000..9ece580ca --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/RowMapping.java @@ -0,0 +1,33 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util; + +import org.mybatis.dynamic.sql.SqlColumn; + +public class RowMapping extends AbstractColumnMapping { + private RowMapping(SqlColumn column) { + super(column); + } + + public static RowMapping of(SqlColumn column) { + return new RowMapping(column); + } + + @Override + public R accept(ColumnMappingVisitor visitor) { + return visitor.visit(this); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/SelectMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/SelectMapping.java index 304540873..b588ece67 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/SelectMapping.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/SelectMapping.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -18,21 +18,21 @@ import org.mybatis.dynamic.sql.SqlColumn; import org.mybatis.dynamic.sql.select.SelectModel; -public class SelectMapping extends AbstractColumnMapping implements UpdateMapping { +public class SelectMapping extends AbstractColumnMapping { + + private final SelectModel selectModel; - private SelectModel selectModel; - private SelectMapping(SqlColumn column, Buildable selectModelBuilder) { super(column); selectModel = selectModelBuilder.build(); } - + public SelectModel selectModel() { return selectModel; } @Override - public R accept(UpdateMappingVisitor visitor) { + public R accept(ColumnMappingVisitor visitor) { return visitor.visit(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/util/SqlProviderAdapter.java b/src/main/java/org/mybatis/dynamic/sql/util/SqlProviderAdapter.java index e645334d5..102a54a75 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/SqlProviderAdapter.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/SqlProviderAdapter.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,7 +15,11 @@ */ package org.mybatis.dynamic.sql.util; +import java.util.List; +import java.util.Map; + import org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider; +import org.mybatis.dynamic.sql.insert.render.GeneralInsertStatementProvider; import org.mybatis.dynamic.sql.insert.render.InsertSelectStatementProvider; import org.mybatis.dynamic.sql.insert.render.InsertStatementProvider; import org.mybatis.dynamic.sql.insert.render.MultiRowInsertStatementProvider; @@ -24,28 +28,66 @@ /** * Adapter for use with MyBatis SQL provider annotations. - * - * @author Jeff Butler * + * @author Jeff Butler */ public class SqlProviderAdapter { public String delete(DeleteStatementProvider deleteStatement) { return deleteStatement.getDeleteStatement(); } - + + public String generalInsert(GeneralInsertStatementProvider insertStatement) { + return insertStatement.getInsertStatement(); + } + public String insert(InsertStatementProvider insertStatement) { return insertStatement.getInsertStatement(); } - + public String insertMultiple(MultiRowInsertStatementProvider insertStatement) { return insertStatement.getInsertStatement(); } - + + /** + * This adapter method is intended for use with MyBatis' @InsertProvider annotation when there are generated + * values expected from executing the insert statement. The canonical method signature for using this adapter method + * is as follows: + * + *

+     * public interface FooMapper {
+     *     @InsertProvider(type=SqlProviderAdapter.class, method="insertMultipleWithGeneratedKeys")
+     *     @Options(useGeneratedKeys=true, keyProperty="records.id")
+     *     int insertMultiple(String insertStatement, @Param("records") List<Foo> records)
+     * }
+     * 
+ * + * @param parameterMap + * The parameter map is automatically created by MyBatis when there are multiple parameters in the insert + * method. + * + * @return the SQL statement contained in the parameter map. This is assumed to be the one and only map entry of + * type String. + */ + public String insertMultipleWithGeneratedKeys(Map parameterMap) { + List entries = parameterMap.entrySet().stream() + .filter(e -> e.getKey().startsWith("param")) //$NON-NLS-1$ + .map(Map.Entry::getValue) + .filter(String.class::isInstance) + .map(String.class::cast) + .toList(); + + if (entries.size() == 1) { + return entries.get(0); + } else { + throw new IllegalArgumentException(Messages.getString("ERROR.30")); //$NON-NLS-1$ + } + } + public String insertSelect(InsertSelectStatementProvider insertStatement) { return insertStatement.getInsertStatement(); } - + public String select(SelectStatementProvider selectStatement) { return selectStatement.getSelectStatement(); } diff --git a/src/main/java/org/mybatis/dynamic/sql/util/StringConstantMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/StringConstantMapping.java index f213fdaa2..c462bd93c 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/StringConstantMapping.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/StringConstantMapping.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -18,36 +18,29 @@ import org.mybatis.dynamic.sql.SqlColumn; /** - * This class represents a mapping between a column and a string constant. The constant should be rendered - * surrounded by single quotes for SQL. - * - * @author Jeff Butler + * This class represents a mapping between a column and a string constant. The constant should be rendered surrounded by + * single quotes for SQL. * + * @author Jeff Butler */ -public class StringConstantMapping extends AbstractColumnMapping implements InsertMapping, UpdateMapping { - private String constant; - - private StringConstantMapping(SqlColumn column) { +public class StringConstantMapping extends AbstractColumnMapping { + private final String constant; + + private StringConstantMapping(SqlColumn column, String constant) { super(column); + this.constant = constant; } public String constant() { return constant; } - - public static StringConstantMapping of(SqlColumn column, String constant) { - StringConstantMapping mapping = new StringConstantMapping(column); - mapping.constant = constant; - return mapping; - } - @Override - public R accept(UpdateMappingVisitor visitor) { - return visitor.visit(this); + public static StringConstantMapping of(SqlColumn column, String constant) { + return new StringConstantMapping(column, constant); } @Override - public R accept(InsertMappingVisitor visitor) { + public R accept(ColumnMappingVisitor visitor) { return visitor.visit(this); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/util/StringUtilities.java b/src/main/java/org/mybatis/dynamic/sql/util/StringUtilities.java index 6875a9821..aad161f0c 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/StringUtilities.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/StringUtilities.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,36 +15,52 @@ */ package org.mybatis.dynamic.sql.util; -import java.util.Optional; -import java.util.function.UnaryOperator; -import java.util.stream.Stream; - public interface StringUtilities { - static String spaceAfter(Optional in) { - return in.map(s -> s + " ") //$NON-NLS-1$ - .orElse(""); //$NON-NLS-1$ - } - static String spaceAfter(String in) { return in + " "; //$NON-NLS-1$ } - - static String spaceBefore(Optional in) { - return in.map(s -> " " + s) //$NON-NLS-1$ - .orElse(""); //$NON-NLS-1$ - } static String spaceBefore(String in) { return " " + in; //$NON-NLS-1$ } - - static String safelyUpperCase(String s) { - return s == null ? null : s.toUpperCase(); + + static String toCamelCase(String inputString) { + StringBuilder sb = new StringBuilder(); + + boolean nextUpperCase = false; + + for (int i = 0; i < inputString.length(); i++) { + char c = inputString.charAt(i); + if (Character.isLetterOrDigit(c)) { + if (nextUpperCase) { + sb.append(Character.toUpperCase(c)); + nextUpperCase = false; + } else { + sb.append(Character.toLowerCase(c)); + } + } else { + if (!sb.isEmpty()) { + nextUpperCase = true; + } + } + } + + return sb.toString(); } - static UnaryOperator> upperCaseAfter(UnaryOperator> valueModifier) { - UnaryOperator> ua = s -> s.map(StringUtilities::safelyUpperCase); - return t -> ua.apply(valueModifier.apply(t)); + static String formatConstantForSQL(String in) { + String escaped = in.replace("'", "''"); //$NON-NLS-1$ //$NON-NLS-2$ + return "'" + escaped + "'"; //$NON-NLS-1$ //$NON-NLS-2$ + } + + static T upperCaseIfPossible(T value) { + if (value instanceof String) { + @SuppressWarnings("unchecked") + T t = (T) ((String) value).toUpperCase(); + return t; + } + + return value; } } diff --git a/src/main/java/org/mybatis/dynamic/sql/util/UpdateMappingVisitor.java b/src/main/java/org/mybatis/dynamic/sql/util/UpdateMappingVisitor.java index 18f43602d..b6597685c 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/UpdateMappingVisitor.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/UpdateMappingVisitor.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,16 +15,29 @@ */ package org.mybatis.dynamic.sql.util; -public interface UpdateMappingVisitor { - T visit(NullMapping mapping); +public abstract class UpdateMappingVisitor implements ColumnMappingVisitor { + @Override + public final R visit(PropertyMapping mapping) { + throw new UnsupportedOperationException(Messages.getInternalErrorString(InternalError.INTERNAL_ERROR_10)); + } - T visit(ConstantMapping mapping); + @Override + public final R visit(PropertyWhenPresentMapping mapping) { + throw new UnsupportedOperationException(Messages.getInternalErrorString(InternalError.INTERNAL_ERROR_11)); + } - T visit(StringConstantMapping mapping); + @Override + public final R visit(RowMapping mapping) { + throw new UnsupportedOperationException(Messages.getInternalErrorString(InternalError.INTERNAL_ERROR_15)); + } - T visit(ValueMapping mapping); - - T visit(SelectMapping mapping); + @Override + public R visit(MappedColumnMapping mapping) { + throw new UnsupportedOperationException(Messages.getInternalErrorString(InternalError.INTERNAL_ERROR_19)); + } - T visit(ColumnMapping columnMapping); + @Override + public R visit(MappedColumnWhenPresentMapping mapping) { + throw new UnsupportedOperationException(Messages.getInternalErrorString(InternalError.INTERNAL_ERROR_20)); + } } diff --git a/src/main/java/org/mybatis/dynamic/sql/util/Utilities.java b/src/main/java/org/mybatis/dynamic/sql/util/Utilities.java new file mode 100644 index 000000000..b0c27081e --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/Utilities.java @@ -0,0 +1,33 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util; + +import java.util.Collection; +import java.util.Objects; +import java.util.stream.Stream; + +import org.jspecify.annotations.Nullable; + +public interface Utilities { + static long safelyUnbox(@Nullable Long l) { + return l == null ? 0 : l; + } + + static Stream filterNulls(Collection<@Nullable T> values) { + // this method helps IntelliJ understand intended nullability + return values.stream().filter(Objects::nonNull); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/Validator.java b/src/main/java/org/mybatis/dynamic/sql/util/Validator.java new file mode 100644 index 000000000..7563bfc70 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/Validator.java @@ -0,0 +1,59 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util; + +import java.util.Collection; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.exception.InvalidSqlException; + +public class Validator { + private Validator() {} + + public static void assertNotEmpty(Collection collection, String messageNumber) { + assertFalse(collection.isEmpty(), messageNumber); + } + + public static void assertNotEmpty(Collection collection, String messageNumber, String p1) { + assertFalse(collection.isEmpty(), messageNumber, p1); + } + + public static void assertFalse(boolean condition, String messageNumber) { + if (condition) { + throw new InvalidSqlException(Messages.getString(messageNumber)); + } + } + + public static void assertFalse(boolean condition, String messageNumber, String p1) { + if (condition) { + throw new InvalidSqlException(Messages.getString(messageNumber, p1)); + } + } + + public static void assertTrue(boolean condition, String messageNumber) { + assertFalse(!condition, messageNumber); + } + + public static void assertTrue(boolean condition, String messageNumber, String p1) { + assertFalse(!condition, messageNumber, p1); + } + + public static void assertNull(@Nullable Object object, String messageNumber) { + if (object != null) { + throw new InvalidSqlException(Messages.getString(messageNumber)); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/ValueMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/ValueMapping.java index 5b206eb01..fc8681010 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/ValueMapping.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/ValueMapping.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,25 +15,30 @@ */ package org.mybatis.dynamic.sql.util; +import java.util.Objects; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; import org.mybatis.dynamic.sql.SqlColumn; -public class ValueMapping extends AbstractColumnMapping implements UpdateMapping { +public class ValueMapping extends AbstractColumnMapping { + + private final Supplier valueSupplier; + // keep a reference to the column so we don't lose the type + private final SqlColumn localColumn; - private Supplier valueSupplier; - private ValueMapping(SqlColumn column, Supplier valueSupplier) { super(column); - this.valueSupplier = valueSupplier; + this.valueSupplier = Objects.requireNonNull(valueSupplier); + localColumn = Objects.requireNonNull(column); } - - public T value() { - return valueSupplier.get(); + + public @Nullable Object value() { + return localColumn.convertParameterType(valueSupplier.get()); } @Override - public R accept(UpdateMappingVisitor visitor) { + public R accept(ColumnMappingVisitor visitor) { return visitor.visit(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/util/ValueOrNullMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/ValueOrNullMapping.java new file mode 100644 index 000000000..f39a706ac --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/ValueOrNullMapping.java @@ -0,0 +1,49 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.SqlColumn; + +public class ValueOrNullMapping extends AbstractColumnMapping { + + private final Supplier<@Nullable T> valueSupplier; + // keep a reference to the column so we don't lose the type + private final SqlColumn localColumn; + + private ValueOrNullMapping(SqlColumn column, Supplier<@Nullable T> valueSupplier) { + super(column); + this.valueSupplier = Objects.requireNonNull(valueSupplier); + localColumn = Objects.requireNonNull(column); + } + + public Optional value() { + return Optional.ofNullable(localColumn.convertParameterType(valueSupplier.get())); + } + + @Override + public R accept(ColumnMappingVisitor visitor) { + return visitor.visit(this); + } + + public static ValueOrNullMapping of(SqlColumn column, Supplier<@Nullable T> valueSupplier) { + return new ValueOrNullMapping<>(column, valueSupplier); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/ValueWhenPresentMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/ValueWhenPresentMapping.java new file mode 100644 index 000000000..5420c644e --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/ValueWhenPresentMapping.java @@ -0,0 +1,53 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.SqlColumn; + +public class ValueWhenPresentMapping extends AbstractColumnMapping { + + private final Supplier<@Nullable T> valueSupplier; + // keep a reference to the column so we don't lose the type + private final SqlColumn localColumn; + + private ValueWhenPresentMapping(SqlColumn column, Supplier<@Nullable T> valueSupplier) { + super(column); + this.valueSupplier = Objects.requireNonNull(valueSupplier); + localColumn = Objects.requireNonNull(column); + } + + public Optional value() { + return Optional.ofNullable(valueSupplier.get()).flatMap(this::convert); + } + + private Optional convert(T value) { + return Optional.ofNullable(localColumn.convertParameterType(value)); + } + + @Override + public R accept(ColumnMappingVisitor visitor) { + return visitor.visit(this); + } + + public static ValueWhenPresentMapping of(SqlColumn column, Supplier<@Nullable T> valueSupplier) { + return new ValueWhenPresentMapping<>(column, valueSupplier); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonCountMapper.java b/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonCountMapper.java new file mode 100644 index 000000000..d0b6c580d --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonCountMapper.java @@ -0,0 +1,40 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util.mybatis3; + +import org.apache.ibatis.annotations.SelectProvider; +import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; +import org.mybatis.dynamic.sql.util.SqlProviderAdapter; + +/** + * This is a general purpose MyBatis mapper for count statements. Count statements are select statements that always + * return a long. + * + *

This mapper can be injected as-is into a MyBatis configuration, or it can be extended with existing mappers. + * + * @author Jeff Butler + */ +public interface CommonCountMapper { + /** + * Execute a select statement that returns a long (typically a select(count(*)) statement). This mapper + * assumes the statement returns a single row with a single column that cen be retrieved as a long. + * + * @param selectStatement the select statement + * @return the long value + */ + @SelectProvider(type = SqlProviderAdapter.class, method = "select") + long count(SelectStatementProvider selectStatement); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonDeleteMapper.java b/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonDeleteMapper.java new file mode 100644 index 000000000..bd7cb06dd --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonDeleteMapper.java @@ -0,0 +1,40 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util.mybatis3; + +import org.apache.ibatis.annotations.DeleteProvider; +import org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider; +import org.mybatis.dynamic.sql.util.SqlProviderAdapter; + +/** + * This is a general purpose MyBatis mapper for delete statements. + * + *

This mapper can be injected as-is into a MyBatis configuration, or it can be extended with existing mappers. + * + * @author Jeff Butler + */ +public interface CommonDeleteMapper { + /** + * Execute a delete statement. + * + * @param deleteStatement + * the delete statement + * + * @return the number of rows affected + */ + @DeleteProvider(type = SqlProviderAdapter.class, method = "delete") + int delete(DeleteStatementProvider deleteStatement); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonGeneralInsertMapper.java b/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonGeneralInsertMapper.java new file mode 100644 index 000000000..4dda5cc11 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonGeneralInsertMapper.java @@ -0,0 +1,49 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util.mybatis3; + +import org.apache.ibatis.annotations.InsertProvider; +import org.mybatis.dynamic.sql.insert.render.GeneralInsertStatementProvider; +import org.mybatis.dynamic.sql.insert.render.InsertSelectStatementProvider; +import org.mybatis.dynamic.sql.util.SqlProviderAdapter; + +/** + * This is a general purpose mapper for executing various non-typed insert statements (general inserts and insert + * selects). This mapper is appropriate for insert statements that do NOT expect generated keys. + */ +public interface CommonGeneralInsertMapper { + /** + * Execute an insert statement with input fields supplied directly. + * + * @param insertStatement + * the insert statement + * + * @return the number of rows affected + */ + @InsertProvider(type = SqlProviderAdapter.class, method = "generalInsert") + int generalInsert(GeneralInsertStatementProvider insertStatement); + + /** + * Execute an insert statement with input fields supplied by a select statement. + * + * @param insertSelectStatement + * the insert statement + * + * @return the number of rows affected + */ + @InsertProvider(type = SqlProviderAdapter.class, method = "insertSelect") + int insertSelect(InsertSelectStatementProvider insertSelectStatement); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonInsertMapper.java b/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonInsertMapper.java new file mode 100644 index 000000000..bd864b610 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonInsertMapper.java @@ -0,0 +1,66 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util.mybatis3; + +import java.util.List; + +import org.apache.ibatis.annotations.Flush; +import org.apache.ibatis.annotations.InsertProvider; +import org.apache.ibatis.executor.BatchResult; +import org.mybatis.dynamic.sql.insert.render.InsertStatementProvider; +import org.mybatis.dynamic.sql.insert.render.MultiRowInsertStatementProvider; +import org.mybatis.dynamic.sql.util.SqlProviderAdapter; + +/** + * This is a general purpose mapper for executing various types of insert statements. This mapper is appropriate for + * insert statements that do NOT expect generated keys. + * + * @param + * the type of row associated with this mapper + */ +public interface CommonInsertMapper extends CommonGeneralInsertMapper { + /** + * Execute an insert statement with input fields mapped to values in a POJO. + * + * @param insertStatement + * the insert statement + * + * @return the number of rows affected + */ + @InsertProvider(type = SqlProviderAdapter.class, method = "insert") + int insert(InsertStatementProvider insertStatement); + + /** + * Execute an insert statement that inserts multiple rows. The row values are supplied by mapping to values in a + * List of POJOs. + * + * @param insertStatement + * the insert statement + * + * @return the number of rows affected + */ + @InsertProvider(type = SqlProviderAdapter.class, method = "insertMultiple") + int insertMultiple(MultiRowInsertStatementProvider insertStatement); + + /** + * Flush batched insert statements and return details of the current batch. This is useful when there is no direct + * access to the @link({@link org.apache.ibatis.session.SqlSession}. + * + * @return details about the current batch including update counts, etc. + */ + @Flush + List flush(); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonSelectMapper.java b/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonSelectMapper.java new file mode 100644 index 000000000..8f0f350ee --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonSelectMapper.java @@ -0,0 +1,291 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util.mybatis3; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +import org.apache.ibatis.annotations.SelectProvider; +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; +import org.mybatis.dynamic.sql.util.SqlProviderAdapter; + +/** + * This is a general purpose MyBatis mapper for select statements. It allows you to execute select statements without + * having to write a custom {@link org.apache.ibatis.annotations.ResultMap} for each statement. + * + *

This mapper contains three types of methods: + *

    + *
  • The selectOneMappedRow and selectManyMappedRows methods allow you to use select statements with + * any number of columns. MyBatis will process the rows and return a Map of values, or a List of Maps.
  • + *
  • The selectOne and selectMany methods also allow you to use select statements with any number of columns. + * These methods also allow you to specify a function that will transform a Map of row values into a specific + * object.
  • + *
  • The other methods are for result sets with a single column. There are functions for many + * data types (Integer, Long, String, etc.) There are also functions that return a single value, and Optional value, + * or a List of values.
  • + *
+ * + *

This mapper can be injected as-is into a MyBatis configuration, or it can be extended with existing mappers. + * + * @author Jeff Butler + */ +public interface CommonSelectMapper { + /** + * Select a single row as a Map of values. The row may have any number of columns. + * The Map key will be the column name as returned from the + * database (the key will be aliased if an alias is specified in the select statement). Map entries will be + * of data types determined by the JDBC driver. MyBatis will call ResultSet.getObject() to retrieve + * values from the ResultSet. Reference your JDBC driver documentation to learn about type mappings + * for your specific database. + * + * @param selectStatement the select statement + * @return A Map containing the row values. + */ + @SelectProvider(type = SqlProviderAdapter.class, method = "select") + @Nullable Map selectOneMappedRow(SelectStatementProvider selectStatement); + + /** + * Select a single row of values and then convert the values to a custom type. This is similar + * to the Spring JDBC template method of processing result sets. In this case, MyBatis will first extract + * the row values into a Map, and then a row mapper can retrieve values from the Map and use them + * to construct a custom object. + * + *

See {@link CommonSelectMapper#selectOneMappedRow(SelectStatementProvider)} for details about + * how MyBatis will construct the Map of values. + * + * @param selectStatement the select statement + * @param rowMapper a function that will convert a Map of row values to the desired data type + * @param the datatype of the converted object + * @return the converted object + */ + default @Nullable R selectOne(SelectStatementProvider selectStatement, + Function, R> rowMapper) { + var result = selectOneMappedRow(selectStatement); + return result == null ? null : rowMapper.apply(result); + } + + /** + * Select any number of rows and return a List of Maps containing row values (one Map for each row returned). + * The rows may have any number of columns. + * The Map key will be the column name as returned from the + * database (the key will be aliased if an alias is specified in the select statement). Map entries will be + * of data types determined by the JDBC driver. MyBatis will call ResultSet.getObject() to retrieve + * values from the ResultSet. Reference your JDBC driver documentation to learn about type mappings + * for your specific database. + * + * @param selectStatement the select statement + * @return A List of Maps containing the row values. + */ + @SelectProvider(type = SqlProviderAdapter.class, method = "select") + List> selectManyMappedRows(SelectStatementProvider selectStatement); + + /** + * Select any number of rows and then convert the values to a custom type. This is similar to the + * Spring JDBC template method of processing result sets. In this case, MyBatis will first extract the + * row values into a List of Map, and them a row mapper can retrieve values from the Map and use them + * to construct a custom object for each row. + * + * @param selectStatement the select statement + * @param rowMapper a function that will convert a Map of row values to the desired data type + * @param the datatype of the converted object + * @return the List of converted objects + */ + default List selectMany(SelectStatementProvider selectStatement, + Function, R> rowMapper) { + return selectManyMappedRows(selectStatement).stream() + .map(rowMapper) + .toList(); + } + + /** + * Retrieve a single {@link java.math.BigDecimal} from a result set. The result set must have + * only one column and one or zero rows. The column must be retrievable from the result set + * via the ResultSet.getBigDecimal() method. + * + * @param selectStatement the select statement + * @return the extracted value. May be null if zero rows are returned, or if the returned + * column is null + */ + @SelectProvider(type = SqlProviderAdapter.class, method = "select") + @Nullable BigDecimal selectOneBigDecimal(SelectStatementProvider selectStatement); + + /** + * Retrieve a single {@link java.math.BigDecimal} from a result set. The result set must have + * only one column and one or zero rows. The column must be retrievable from the result set + * via the ResultSet.getBigDecimal() method. + * + * @param selectStatement the select statement + * @return the extracted value. The Optional will be empty if zero rows are returned, or if the returned + * column is null + */ + @SelectProvider(type = SqlProviderAdapter.class, method = "select") + Optional selectOptionalBigDecimal(SelectStatementProvider selectStatement); + + /** + * Retrieve a List of {@link java.math.BigDecimal} from a result set. The result set must have + * only one column, but can have any number of rows. The column must be retrievable from the result set + * via the ResultSet.getBigDecimal() method. + * + * @param selectStatement the select statement + * @return the list of extracted values. Any value may be null if a column in the result set is null + */ + @SelectProvider(type = SqlProviderAdapter.class, method = "select") + List selectManyBigDecimals(SelectStatementProvider selectStatement); + + /** + * Retrieve a single {@link java.lang.Double} from a result set. The result set must have + * only one column and one or zero rows. The column must be retrievable from the result set + * via the ResultSet.getDouble() method. + * + * @param selectStatement the select statement + * @return the extracted value. May be null if zero rows are returned, or if the returned + * column is null + */ + @SelectProvider(type = SqlProviderAdapter.class, method = "select") + @Nullable Double selectOneDouble(SelectStatementProvider selectStatement); + + /** + * Retrieve a single {@link java.lang.Double} from a result set. The result set must have + * only one column and one or zero rows. The column must be retrievable from the result set + * via the ResultSet.getDouble() method. + * + * @param selectStatement the select statement + * @return the extracted value. The Optional will be empty if zero rows are returned, or if the returned + * column is null + */ + @SelectProvider(type = SqlProviderAdapter.class, method = "select") + Optional selectOptionalDouble(SelectStatementProvider selectStatement); + + /** + * Retrieve a List of {@link java.lang.Double} from a result set. The result set must have + * only one column, but can have any number of rows. The column must be retrievable from the result set + * via the ResultSet.getDouble() method. + * + * @param selectStatement the select statement + * @return the list of extracted values. Any value may be null if a column in the result set is null + */ + @SelectProvider(type = SqlProviderAdapter.class, method = "select") + List selectManyDoubles(SelectStatementProvider selectStatement); + + /** + * Retrieve a single {@link java.lang.Integer} from a result set. The result set must have + * only one column and one or zero rows. The column must be retrievable from the result set + * via the ResultSet.getInt() method. + * + * @param selectStatement the select statement + * @return the extracted value. May be null if zero rows are returned, or if the returned + * column is null + */ + @SelectProvider(type = SqlProviderAdapter.class, method = "select") + @Nullable Integer selectOneInteger(SelectStatementProvider selectStatement); + + /** + * Retrieve a single {@link java.lang.Integer} from a result set. The result set must have + * only one column and one or zero rows. The column must be retrievable from the result set + * via the ResultSet.getInt() method. + * + * @param selectStatement the select statement + * @return the extracted value. The Optional will be empty if zero rows are returned, or if the returned + * column is null + */ + @SelectProvider(type = SqlProviderAdapter.class, method = "select") + Optional selectOptionalInteger(SelectStatementProvider selectStatement); + + /** + * Retrieve a List of {@link java.lang.Integer} from a result set. The result set must have + * only one column, but can have any number of rows. The column must be retrievable from the result set + * via the ResultSet.getInt() method. + * + * @param selectStatement the select statement + * @return the list of extracted values. Any value may be null if a column in the result set is null + */ + @SelectProvider(type = SqlProviderAdapter.class, method = "select") + List selectManyIntegers(SelectStatementProvider selectStatement); + + /** + * Retrieve a single {@link java.lang.Long} from a result set. The result set must have + * only one column and one or zero rows. The column must be retrievable from the result set + * via the ResultSet.getLong() method. + * + * @param selectStatement the select statement + * @return the extracted value. May be null if zero rows are returned, or if the returned + * column is null + */ + @SelectProvider(type = SqlProviderAdapter.class, method = "select") + @Nullable Long selectOneLong(SelectStatementProvider selectStatement); + + /** + * Retrieve a single {@link java.lang.Long} from a result set. The result set must have + * only one column and one or zero rows. The column must be retrievable from the result set + * via the ResultSet.getLong() method. + * + * @param selectStatement the select statement + * @return the extracted value. The Optional will be empty if zero rows are returned, or if the returned + * column is null + */ + @SelectProvider(type = SqlProviderAdapter.class, method = "select") + Optional selectOptionalLong(SelectStatementProvider selectStatement); + + /** + * Retrieve a List of {@link java.lang.Long} from a result set. The result set must have + * only one column, but can have any number of rows. The column must be retrievable from the result set + * via the ResultSet.getLong() method. + * + * @param selectStatement the select statement + * @return the list of extracted values. Any value may be null if a column in the result set is null + */ + @SelectProvider(type = SqlProviderAdapter.class, method = "select") + List selectManyLongs(SelectStatementProvider selectStatement); + + /** + * Retrieve a single {@link java.lang.String} from a result set. The result set must have + * only one column and one or zero rows. The column must be retrievable from the result set + * via the ResultSet.getString() method. + * + * @param selectStatement the select statement + * @return the extracted value. May be null if zero rows are returned, or if the returned + * column is null + */ + @SelectProvider(type = SqlProviderAdapter.class, method = "select") + @Nullable String selectOneString(SelectStatementProvider selectStatement); + + /** + * Retrieve a single {@link java.lang.String} from a result set. The result set must have + * only one column and one or zero rows. The column must be retrievable from the result set + * via the ResultSet.getString() method. + * + * @param selectStatement the select statement + * @return the extracted value. The Optional will be empty if zero rows are returned, or if the returned + * column is null + */ + @SelectProvider(type = SqlProviderAdapter.class, method = "select") + Optional selectOptionalString(SelectStatementProvider selectStatement); + + /** + * Retrieve a List of {@link java.lang.String} from a result set. The result set must have + * only one column, but can have any number of rows. The column must be retrievable from the result set + * via the ResultSet.getString() method. + * + * @param selectStatement the select statement + * @return the list of extracted values. Any value may be null if a column in the result set is null + */ + @SelectProvider(type = SqlProviderAdapter.class, method = "select") + List selectManyStrings(SelectStatementProvider selectStatement); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonUpdateMapper.java b/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonUpdateMapper.java new file mode 100644 index 000000000..0f25b575d --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonUpdateMapper.java @@ -0,0 +1,40 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util.mybatis3; + +import org.apache.ibatis.annotations.UpdateProvider; +import org.mybatis.dynamic.sql.update.render.UpdateStatementProvider; +import org.mybatis.dynamic.sql.util.SqlProviderAdapter; + +/** + * This is a general purpose MyBatis mapper for update statements. + * + *

This mapper can be injected as-is into a MyBatis configuration, or it can be extended with existing mappers. + * + * @author Jeff Butler + */ +public interface CommonUpdateMapper { + /** + * Execute an update statement. + * + * @param updateStatement + * the update statement + * + * @return the number of rows affected + */ + @UpdateProvider(type = SqlProviderAdapter.class, method = "update") + int update(UpdateStatementProvider updateStatement); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/MyBatis3Utils.java b/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/MyBatis3Utils.java index 0eb5223b5..2e2bb279d 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/MyBatis3Utils.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/MyBatis3Utils.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -18,6 +18,7 @@ import java.util.Collection; import java.util.List; import java.util.function.Function; +import java.util.function.ToIntBiFunction; import java.util.function.ToIntFunction; import java.util.function.ToLongFunction; import java.util.function.UnaryOperator; @@ -27,8 +28,10 @@ import org.mybatis.dynamic.sql.SqlTable; import org.mybatis.dynamic.sql.delete.DeleteDSLCompleter; import org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider; +import org.mybatis.dynamic.sql.insert.GeneralInsertDSL; import org.mybatis.dynamic.sql.insert.InsertDSL; import org.mybatis.dynamic.sql.insert.MultiRowInsertDSL; +import org.mybatis.dynamic.sql.insert.render.GeneralInsertStatementProvider; import org.mybatis.dynamic.sql.insert.render.InsertStatementProvider; import org.mybatis.dynamic.sql.insert.render.MultiRowInsertStatementProvider; import org.mybatis.dynamic.sql.render.RenderingStrategies; @@ -43,13 +46,31 @@ /** * Utility functions for building MyBatis3 mappers. - * - * @author Jeff Butler * + * @author Jeff Butler */ public class MyBatis3Utils { private MyBatis3Utils() {} + public static long count(ToLongFunction mapper, BasicColumn column, SqlTable table, + CountDSLCompleter completer) { + return mapper.applyAsLong(count(column, table, completer)); + } + + public static SelectStatementProvider count(BasicColumn column, SqlTable table, CountDSLCompleter completer) { + return countFrom(SqlBuilder.countColumn(column).from(table), completer); + } + + public static long countDistinct(ToLongFunction mapper, BasicColumn column, SqlTable table, + CountDSLCompleter completer) { + return mapper.applyAsLong(countDistinct(column, table, completer)); + } + + public static SelectStatementProvider countDistinct(BasicColumn column, SqlTable table, + CountDSLCompleter completer) { + return countFrom(SqlBuilder.countDistinctColumn(column).from(table), completer); + } + public static SelectStatementProvider countFrom(SqlTable table, CountDSLCompleter completer) { return countFrom(SqlBuilder.countFrom(table), completer); } @@ -80,19 +101,31 @@ public static int deleteFrom(ToIntFunction mapper, SqlTable table, DeleteDSLCompleter completer) { return mapper.applyAsInt(deleteFrom(table, completer)); } - - public static InsertStatementProvider insert(R record, SqlTable table, + + public static InsertStatementProvider insert(R row, SqlTable table, UnaryOperator> completer) { - return completer.apply(SqlBuilder.insert(record).into(table)) + return completer.apply(SqlBuilder.insert(row).into(table)) .build() .render(RenderingStrategies.MYBATIS3); } - public static int insert(ToIntFunction> mapper, R record, + public static int insert(ToIntFunction> mapper, R row, SqlTable table, UnaryOperator> completer) { - return mapper.applyAsInt(insert(record, table, completer)); + return mapper.applyAsInt(insert(row, table, completer)); } - + + public static GeneralInsertStatementProvider generalInsert(SqlTable table, + UnaryOperator completer) { + return completer.apply(GeneralInsertDSL.insertInto(table)) + .build() + .render(RenderingStrategies.MYBATIS3); + } + + public static int generalInsert(ToIntFunction mapper, + SqlTable table, UnaryOperator completer) { + return mapper.applyAsInt(generalInsert(table, completer)); + } + public static MultiRowInsertStatementProvider insertMultiple(Collection records, SqlTable table, UnaryOperator> completer) { return completer.apply(SqlBuilder.insertMultiple(records).into(table)) @@ -105,6 +138,12 @@ public static int insertMultiple(ToIntFunction int insertMultipleWithGeneratedKeys(ToIntBiFunction> mapper, + Collection records, SqlTable table, UnaryOperator> completer) { + MultiRowInsertStatementProvider provider = insertMultiple(records, table, completer); + return mapper.applyAsInt(provider.getInsertStatement(), provider.getRecords()); + } + public static SelectStatementProvider select(BasicColumn[] selectList, SqlTable table, SelectDSLCompleter completer) { return select(SqlBuilder.select(selectList).from(table), completer); @@ -143,7 +182,7 @@ public static R selectOne(Function mapper, } public static R selectOne(Function mapper, - QueryExpressionDSL start,SelectDSLCompleter completer) { + QueryExpressionDSL start, SelectDSLCompleter completer) { return mapper.apply(select(start, completer)); } diff --git a/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/package-info.java b/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/package-info.java new file mode 100644 index 000000000..3eda4b115 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +@NullMarked +package org.mybatis.dynamic.sql.util.mybatis3; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/mybatis/dynamic/sql/util/package-info.java b/src/main/java/org/mybatis/dynamic/sql/util/package-info.java new file mode 100644 index 000000000..82bcfdd13 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +@NullMarked +package org.mybatis.dynamic.sql.util; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/mybatis/dynamic/sql/util/spring/BatchInsertUtility.java b/src/main/java/org/mybatis/dynamic/sql/util/spring/BatchInsertUtility.java new file mode 100644 index 000000000..598a6b4a0 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/spring/BatchInsertUtility.java @@ -0,0 +1,43 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util.spring; + +import java.util.List; + +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils; + +/** + * Utility class for converting a list of rows to an array or SqlParameterSources. + * + *

This class is necessary due to the way that the library generates bindings for batch insert + * statements. The bindings will be of the form :row.propertyName. The createBatch method + * in this class will wrap all input rows in a class - RowHolder - with a single property named "row". + * This will allow the generated bindings to function properly with a Spring batch insert. + */ +public class BatchInsertUtility { + private BatchInsertUtility() {} + + public static SqlParameterSource[] createBatch(List rows) { + List> tt = rows.stream() + .map(RowHolder::new) + .toList(); + + return SqlParameterSourceUtils.createBatch(tt); + } + + public record RowHolder(T row) {} +} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/spring/NamedParameterJdbcTemplateExtensions.java b/src/main/java/org/mybatis/dynamic/sql/util/spring/NamedParameterJdbcTemplateExtensions.java new file mode 100644 index 000000000..4e630e1d2 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/spring/NamedParameterJdbcTemplateExtensions.java @@ -0,0 +1,165 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util.spring; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.mybatis.dynamic.sql.delete.DeleteModel; +import org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider; +import org.mybatis.dynamic.sql.insert.BatchInsertModel; +import org.mybatis.dynamic.sql.insert.GeneralInsertModel; +import org.mybatis.dynamic.sql.insert.InsertModel; +import org.mybatis.dynamic.sql.insert.MultiRowInsertModel; +import org.mybatis.dynamic.sql.insert.render.BatchInsert; +import org.mybatis.dynamic.sql.insert.render.GeneralInsertStatementProvider; +import org.mybatis.dynamic.sql.insert.render.InsertStatementProvider; +import org.mybatis.dynamic.sql.insert.render.MultiRowInsertStatementProvider; +import org.mybatis.dynamic.sql.render.RenderingStrategies; +import org.mybatis.dynamic.sql.select.SelectModel; +import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; +import org.mybatis.dynamic.sql.update.UpdateModel; +import org.mybatis.dynamic.sql.update.render.UpdateStatementProvider; +import org.mybatis.dynamic.sql.util.Buildable; +import org.mybatis.dynamic.sql.util.Utilities; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.support.KeyHolder; + +public class NamedParameterJdbcTemplateExtensions { + private final NamedParameterJdbcTemplate template; + + public NamedParameterJdbcTemplateExtensions(NamedParameterJdbcTemplate template) { + this.template = Objects.requireNonNull(template); + } + + public long count(Buildable countStatement) { + return count(countStatement.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER)); + } + + public long count(SelectStatementProvider countStatement) { + Long answer = template.queryForObject(countStatement.getSelectStatement(), + countStatement.getParameters(), Long.class); + + return Utilities.safelyUnbox(answer); + } + + public int delete(Buildable deleteStatement) { + return delete(deleteStatement.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER)); + } + + public int delete(DeleteStatementProvider deleteStatement) { + return template.update(deleteStatement.getDeleteStatement(), deleteStatement.getParameters()); + } + + public int generalInsert(Buildable insertStatement) { + return generalInsert(insertStatement.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER)); + } + + public int generalInsert(GeneralInsertStatementProvider insertStatement) { + return template.update(insertStatement.getInsertStatement(), insertStatement.getParameters()); + } + + public int generalInsert(Buildable insertStatement, KeyHolder keyHolder) { + return generalInsert(insertStatement.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER), keyHolder); + } + + public int generalInsert(GeneralInsertStatementProvider insertStatement, KeyHolder keyHolder) { + return template.update(insertStatement.getInsertStatement(), + new MapSqlParameterSource(insertStatement.getParameters()), keyHolder); + } + + public int insert(Buildable> insertStatement) { + return insert(insertStatement.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER)); + } + + public int insert(InsertStatementProvider insertStatement) { + return template.update(insertStatement.getInsertStatement(), + new BeanPropertySqlParameterSource(insertStatement)); + } + + public int insert(Buildable> insertStatement, KeyHolder keyHolder) { + return insert(insertStatement.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER), keyHolder); + } + + public int insert(InsertStatementProvider insertStatement, KeyHolder keyHolder) { + return template.update(insertStatement.getInsertStatement(), + new BeanPropertySqlParameterSource(insertStatement), keyHolder); + } + + public int[] insertBatch(Buildable> insertStatement) { + return insertBatch(insertStatement.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER)); + } + + public int[] insertBatch(BatchInsert insertStatement) { + return template.batchUpdate(insertStatement.getInsertStatementSQL(), + BatchInsertUtility.createBatch(insertStatement.getRecords())); + } + + public int insertMultiple(Buildable> insertStatement) { + return insertMultiple(insertStatement.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER)); + } + + public int insertMultiple(MultiRowInsertStatementProvider insertStatement) { + return template.update(insertStatement.getInsertStatement(), + new BeanPropertySqlParameterSource(insertStatement)); + } + + public int insertMultiple(Buildable> insertStatement, KeyHolder keyHolder) { + return insertMultiple(insertStatement.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER), keyHolder); + } + + public int insertMultiple(MultiRowInsertStatementProvider insertStatement, KeyHolder keyHolder) { + return template.update(insertStatement.getInsertStatement(), + new BeanPropertySqlParameterSource(insertStatement), keyHolder); + } + + public List selectList(Buildable selectStatement, RowMapper rowMapper) { + return selectList(selectStatement.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER), rowMapper); + } + + public List selectList(SelectStatementProvider selectStatement, RowMapper rowMapper) { + return template.query(selectStatement.getSelectStatement(), selectStatement.getParameters(), rowMapper); + } + + public Optional selectOne(Buildable selectStatement, RowMapper rowMapper) { + return selectOne(selectStatement.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER), rowMapper); + } + + public Optional selectOne(SelectStatementProvider selectStatement, RowMapper rowMapper) { + T result; + try { + result = template.queryForObject(selectStatement.getSelectStatement(), selectStatement.getParameters(), + rowMapper); + } catch (EmptyResultDataAccessException e) { + result = null; + } + + return Optional.ofNullable(result); + } + + public int update(Buildable updateStatement) { + return update(updateStatement.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER)); + } + + public int update(UpdateStatementProvider updateStatement) { + return template.update(updateStatement.getUpdateStatement(), updateStatement.getParameters()); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/spring/package-info.java b/src/main/java/org/mybatis/dynamic/sql/util/spring/package-info.java new file mode 100644 index 000000000..1c53822b8 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/spring/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +@NullMarked +package org.mybatis.dynamic.sql.util.spring; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchCursorReaderSelectModel.java b/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchCursorReaderSelectModel.java deleted file mode 100644 index 58543bc0d..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchCursorReaderSelectModel.java +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright 2016-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.util.springbatch; - -import org.mybatis.dynamic.sql.select.SelectModel; -import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; - -public class SpringBatchCursorReaderSelectModel { - - private SelectModel selectModel; - - public SpringBatchCursorReaderSelectModel(SelectModel selectModel) { - this.selectModel = selectModel; - } - - public SelectStatementProvider render() { - return selectModel.render(SpringBatchUtility.SPRING_BATCH_READER_RENDERING_STRATEGY); - } -} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchPagingItemReaderRenderingStrategy.java b/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchPagingItemReaderRenderingStrategy.java new file mode 100644 index 000000000..f56063d95 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchPagingItemReaderRenderingStrategy.java @@ -0,0 +1,50 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util.springbatch; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.mybatis.dynamic.sql.render.MyBatis3RenderingStrategy; + +/** + * This rendering strategy should be used for MyBatis3 statements using the + * MyBatisPagingItemReader supplied by mybatis-spring integration + * (http://www.mybatis.org/spring/). + */ +public class SpringBatchPagingItemReaderRenderingStrategy extends MyBatis3RenderingStrategy { + + @Override + public String getFormattedJdbcPlaceholderForPagingParameters(String prefix, String parameterName) { + return "#{" //$NON-NLS-1$ + + parameterName + + "}"; //$NON-NLS-1$ + } + + @Override + public String formatParameterMapKeyForFetchFirstRows(AtomicInteger sequence) { + return "_pagesize"; //$NON-NLS-1$ + } + + @Override + public String formatParameterMapKeyForLimit(AtomicInteger sequence) { + return "_pagesize"; //$NON-NLS-1$ + } + + @Override + public String formatParameterMapKeyForOffset(AtomicInteger sequence) { + return "_skiprows"; //$NON-NLS-1$ + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchPagingReaderSelectModel.java b/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchPagingReaderSelectModel.java deleted file mode 100644 index 132d04e94..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchPagingReaderSelectModel.java +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright 2016-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.util.springbatch; - -import java.util.HashMap; -import java.util.Map; - -import org.mybatis.dynamic.sql.select.SelectModel; -import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; - -public class SpringBatchPagingReaderSelectModel { - - private SelectModel selectModel; - - public SpringBatchPagingReaderSelectModel(SelectModel selectModel) { - this.selectModel = selectModel; - } - - public SelectStatementProvider render() { - SelectStatementProvider selectStatement = - selectModel.render(SpringBatchUtility.SPRING_BATCH_READER_RENDERING_STRATEGY); - return new LimitAndOffsetDecorator(selectStatement); - } - - public static class LimitAndOffsetDecorator implements SelectStatementProvider { - private Map parameters = new HashMap<>(); - private String selectStatement; - - public LimitAndOffsetDecorator(SelectStatementProvider delegate) { - parameters.putAll(delegate.getParameters()); - - selectStatement = delegate.getSelectStatement() - + " LIMIT #{_pagesize} OFFSET #{_skiprows}"; //$NON-NLS-1$ - } - - @Override - public Map getParameters() { - return parameters; - } - - @Override - public String getSelectStatement() { - return selectStatement; - } - } -} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchProviderAdapter.java b/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchProviderAdapter.java index fc183823e..2000c02bf 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchProviderAdapter.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchProviderAdapter.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -17,13 +17,9 @@ import java.util.Map; -import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; - public class SpringBatchProviderAdapter { public String select(Map parameterValues) { - SelectStatementProvider selectStatement = - (SelectStatementProvider) parameterValues.get(SpringBatchUtility.PARAMETER_KEY); - return selectStatement.getSelectStatement(); + return (String) parameterValues.get(SpringBatchUtility.PARAMETER_KEY); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchUtility.java b/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchUtility.java index 629f3ce27..9b84ee499 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchUtility.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchUtility.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -18,48 +18,45 @@ import java.util.HashMap; import java.util.Map; -import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.render.RenderingStrategy; -import org.mybatis.dynamic.sql.select.QueryExpressionDSL; -import org.mybatis.dynamic.sql.select.SelectDSL; import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; public class SpringBatchUtility { private SpringBatchUtility() {} - - public static final String PARAMETER_KEY = "mybatis3_dsql_query"; //$NON-NLS-1$ - - public static final RenderingStrategy SPRING_BATCH_READER_RENDERING_STRATEGY = - new SpringBatchReaderRenderingStrategy(); - - public static Map toParameterValues(SelectStatementProvider selectStatement) { - Map parameterValues = new HashMap<>(); - parameterValues.put(PARAMETER_KEY, selectStatement); - return parameterValues; - } + + static final String PARAMETER_KEY = "mybatis3_dsql_query"; //$NON-NLS-1$ /** - * Select builder that renders in a manner appropriate for the MyBatisPagingItemReader. - * - * Important rendered SQL will contain LIMIT and OFFSET clauses in the SELECT statement. If your database - * (Oracle) does not support LIMIT and OFFSET, the queries will fail. - * - * @param selectList a column list for the SELECT statement - * @return FromGatherer used to continue a SELECT statement + * Constant for use in a query intended for use with the MyBatisPagingItemReader. + * This value will not be used in the query at runtime because MyBatis Spring integration + * will supply a value for _skiprows. + * + *

This value can be used as a parameter for the "offset" method in a query to make the intention + * clear that the actual runtime value will be supplied by MyBatis Spring integration. + * + *

See https://mybatis.org/spring/batch.html for details. */ - public static QueryExpressionDSL.FromGatherer selectForPaging( - BasicColumn...selectList) { - return SelectDSL.select(SpringBatchPagingReaderSelectModel::new, selectList); - } + public static final long MYBATIS_SPRING_BATCH_SKIPROWS = -437L; /** - * Select builder that renders in a manner appropriate for the MyBatisCursorItemReader. - * - * @param selectList a column list for the SELECT statement - * @return FromGatherer used to continue a SELECT statement + * Constant for use in a query intended for use with the MyBatisPagingItemReader. + * This value will not be used in the query at runtime because MyBatis Spring integration + * will supply a value for _pagesize. + * + *

This value can be used as a parameter for the "limit" or "fetchFirst" method in a query to make the intention + * clear that the actual runtime value will be supplied by MyBatis Spring integration. + * + *

See https://mybatis.org/spring/batch.html for details. */ - public static QueryExpressionDSL.FromGatherer selectForCursor( - BasicColumn...selectList) { - return SelectDSL.select(SpringBatchCursorReaderSelectModel::new, selectList); + public static final long MYBATIS_SPRING_BATCH_PAGESIZE = -439L; + + public static final RenderingStrategy SPRING_BATCH_PAGING_ITEM_READER_RENDERING_STRATEGY = + new SpringBatchPagingItemReaderRenderingStrategy(); + + public static Map toParameterValues(SelectStatementProvider selectStatement) { + var parameterValues = new HashMap(); + parameterValues.put(PARAMETER_KEY, selectStatement.getSelectStatement()); + parameterValues.put(RenderingStrategy.DEFAULT_PARAMETER_PREFIX, selectStatement.getParameters()); + return parameterValues; } } diff --git a/src/main/java/org/mybatis/dynamic/sql/util/springbatch/package-info.java b/src/main/java/org/mybatis/dynamic/sql/util/springbatch/package-info.java new file mode 100644 index 000000000..16a66a9fe --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/springbatch/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +@NullMarked +package org.mybatis.dynamic.sql.util.springbatch; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereDSL.java b/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereDSL.java deleted file mode 100644 index c75417d4f..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereDSL.java +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Copyright 2016-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.where; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import org.mybatis.dynamic.sql.BindableColumn; -import org.mybatis.dynamic.sql.SqlCriterion; -import org.mybatis.dynamic.sql.VisitableCondition; - -public abstract class AbstractWhereDSL> { - private List> criteria = new ArrayList<>(); - - protected AbstractWhereDSL() { - super(); - } - - public T where(BindableColumn column, VisitableCondition condition) { - addCriterion(column, condition); - return getThis(); - } - - public T where(BindableColumn column, VisitableCondition condition, SqlCriterion...subCriteria) { - addCriterion(column, condition, Arrays.asList(subCriteria)); - return getThis(); - } - - public T where(BindableColumn column, VisitableCondition condition, List> subCriteria) { - addCriterion(column, condition, subCriteria); - return getThis(); - } - - @SuppressWarnings("unchecked") - public T applyWhere(WhereApplier whereApplier) { - return (T) whereApplier.apply(this); - } - - public T and(BindableColumn column, VisitableCondition condition) { - addCriterion("and", column, condition); //$NON-NLS-1$ - return getThis(); - } - - public T and(BindableColumn column, VisitableCondition condition, SqlCriterion...subCriteria) { - addCriterion("and", column, condition, Arrays.asList(subCriteria)); //$NON-NLS-1$ - return getThis(); - } - - public T and(BindableColumn column, VisitableCondition condition, List> subCriteria) { - addCriterion("and", column, condition, subCriteria); //$NON-NLS-1$ - return getThis(); - } - - public T or(BindableColumn column, VisitableCondition condition) { - addCriterion("or", column, condition); //$NON-NLS-1$ - return getThis(); - } - - public T or(BindableColumn column, VisitableCondition condition, SqlCriterion...subCriteria) { - addCriterion("or", column, condition, Arrays.asList(subCriteria)); //$NON-NLS-1$ - return getThis(); - } - - public T or(BindableColumn column, VisitableCondition condition, List> subCriteria) { - addCriterion("or", column, condition, subCriteria); //$NON-NLS-1$ - return getThis(); - } - - private void addCriterion(BindableColumn column, VisitableCondition condition) { - SqlCriterion criterion = SqlCriterion.withColumn(column) - .withCondition(condition) - .build(); - criteria.add(criterion); - } - - private void addCriterion(String connector, BindableColumn column, VisitableCondition condition) { - SqlCriterion criterion = SqlCriterion.withColumn(column) - .withConnector(connector) - .withCondition(condition) - .build(); - criteria.add(criterion); - } - - private void addCriterion(BindableColumn column, VisitableCondition condition, - List> subCriteria) { - SqlCriterion criterion = SqlCriterion.withColumn(column) - .withCondition(condition) - .withSubCriteria(subCriteria) - .build(); - criteria.add(criterion); - } - - private void addCriterion(String connector, BindableColumn column, VisitableCondition condition, - List> subCriteria) { - SqlCriterion criterion = SqlCriterion.withColumn(column) - .withConnector(connector) - .withCondition(condition) - .withSubCriteria(subCriteria) - .build(); - criteria.add(criterion); - } - - protected WhereModel internalBuild() { - return WhereModel.of(criteria); - } - - protected abstract WhereModel buildWhereModel(); - - protected abstract T getThis(); -} diff --git a/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereFinisher.java b/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereFinisher.java new file mode 100644 index 000000000..260fef7ea --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereFinisher.java @@ -0,0 +1,60 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.where; + +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.AndOrCriteriaGroup; +import org.mybatis.dynamic.sql.SqlCriterion; +import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionDSL; +import org.mybatis.dynamic.sql.configuration.StatementConfiguration; +import org.mybatis.dynamic.sql.util.ConfigurableStatement; + +public abstract class AbstractWhereFinisher> extends AbstractBooleanExpressionDSL + implements ConfigurableStatement { + private final ConfigurableStatement parentStatement; + + protected AbstractWhereFinisher(ConfigurableStatement parentStatement) { + this.parentStatement = Objects.requireNonNull(parentStatement); + } + + void initialize(SqlCriterion sqlCriterion) { + setInitialCriterion(sqlCriterion, StatementType.WHERE); + } + + void initialize(@Nullable SqlCriterion sqlCriterion, List subCriteria) { + setInitialCriterion(sqlCriterion, StatementType.WHERE); + super.subCriteria.addAll(subCriteria); + } + + @NonNull + @Override + public T configureStatement(Consumer consumer) { + parentStatement.configureStatement(consumer); + return getThis(); + } + + protected EmbeddedWhereModel buildModel() { + return new EmbeddedWhereModel.Builder() + .withInitialCriterion(getInitialCriterion()) + .withSubCriteria(subCriteria) + .build(); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereStarter.java b/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereStarter.java new file mode 100644 index 000000000..17efa091e --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereStarter.java @@ -0,0 +1,103 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.where; + +import java.util.Arrays; +import java.util.List; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.AndOrCriteriaGroup; +import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.ColumnAndConditionCriterion; +import org.mybatis.dynamic.sql.CriteriaGroup; +import org.mybatis.dynamic.sql.ExistsCriterion; +import org.mybatis.dynamic.sql.ExistsPredicate; +import org.mybatis.dynamic.sql.RenderableCondition; +import org.mybatis.dynamic.sql.SqlCriterion; +import org.mybatis.dynamic.sql.util.ConfigurableStatement; + +/** + * Base class for DSLs that support where clauses - which is every DSL except Insert. + * The purpose of the class is to provide a common set of where methods that can be used by + * any statement. + * + * @param the implementation of the Where DSL customized for a particular SQL statement. + */ +public interface AbstractWhereStarter, D extends AbstractWhereStarter> + extends ConfigurableStatement { + + default F where(BindableColumn column, RenderableCondition condition, AndOrCriteriaGroup... subCriteria) { + return where(column, condition, Arrays.asList(subCriteria)); + } + + default F where(BindableColumn column, RenderableCondition condition, + List subCriteria) { + SqlCriterion sqlCriterion = ColumnAndConditionCriterion.withColumn(column) + .withCondition(condition) + .withSubCriteria(subCriteria) + .build(); + + return initialize(sqlCriterion); + } + + default F where(ExistsPredicate existsPredicate, AndOrCriteriaGroup... subCriteria) { + return where(existsPredicate, Arrays.asList(subCriteria)); + } + + default F where(ExistsPredicate existsPredicate, List subCriteria) { + ExistsCriterion sqlCriterion = new ExistsCriterion.Builder() + .withExistsPredicate(existsPredicate) + .withSubCriteria(subCriteria) + .build(); + + return initialize(sqlCriterion); + } + + default F where(SqlCriterion initialCriterion, AndOrCriteriaGroup... subCriteria) { + return where(initialCriterion, Arrays.asList(subCriteria)); + } + + default F where(@Nullable SqlCriterion initialCriterion, List subCriteria) { + SqlCriterion sqlCriterion = new CriteriaGroup.Builder() + .withInitialCriterion(initialCriterion) + .withSubCriteria(subCriteria) + .build(); + + return initialize(sqlCriterion); + } + + default F where(List subCriteria) { + SqlCriterion sqlCriterion = new CriteriaGroup.Builder() + .withSubCriteria(subCriteria) + .build(); + + return initialize(sqlCriterion); + } + + F where(); + + default F applyWhere(WhereApplier whereApplier) { + F finisher = where(); + whereApplier.accept(finisher); + return finisher; + } + + private F initialize(SqlCriterion sqlCriterion) { + F finisher = where(); + finisher.initialize(sqlCriterion); + return finisher; + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/where/EmbeddedWhereModel.java b/src/main/java/org/mybatis/dynamic/sql/where/EmbeddedWhereModel.java new file mode 100644 index 000000000..7cd42e73e --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/where/EmbeddedWhereModel.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.where; + +import java.util.Optional; + +import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionModel; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.where.render.WhereRenderer; + +public class EmbeddedWhereModel extends AbstractBooleanExpressionModel { + private EmbeddedWhereModel(Builder builder) { + super(builder); + } + + public Optional render(RenderingContext renderingContext) { + return WhereRenderer.withWhereModel(this) + .withRenderingContext(renderingContext) + .build() + .render(); + } + + public static class Builder extends AbstractBuilder { + public EmbeddedWhereModel build() { + return new EmbeddedWhereModel(this); + } + + @Override + protected Builder getThis() { + return this; + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/where/WhereApplier.java b/src/main/java/org/mybatis/dynamic/sql/where/WhereApplier.java index ed493eb3e..d708aa065 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/WhereApplier.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/WhereApplier.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,7 +15,25 @@ */ package org.mybatis.dynamic.sql.where; -import java.util.function.UnaryOperator; +import java.util.function.Consumer; + +import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionDSL; @FunctionalInterface -public interface WhereApplier extends UnaryOperator> {} +public interface WhereApplier { + + void accept(AbstractWhereFinisher whereFinisher); + + /** + * Return a composed where applier that performs this operation followed by the after operation. + * + * @param after the operation to perform after this operation + * @return a composed where applier that performs this operation followed by the after operation. + */ + default WhereApplier andThen(Consumer> after) { + return t -> { + accept(t); + after.accept(t); + }; + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/where/WhereDSL.java b/src/main/java/org/mybatis/dynamic/sql/where/WhereDSL.java index e9f7e9f46..6a20b57e8 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/WhereDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/WhereDSL.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,27 +15,53 @@ */ package org.mybatis.dynamic.sql.where; -public class WhereDSL extends AbstractWhereDSL { +import java.util.function.Consumer; + +import org.mybatis.dynamic.sql.configuration.StatementConfiguration; +import org.mybatis.dynamic.sql.util.Buildable; + +/** + * DSL for standalone where clauses. + * + *

This can also be used to create reusable where clauses for different statements. + */ +public class WhereDSL implements AbstractWhereStarter { + private final StatementConfiguration statementConfiguration = new StatementConfiguration(); + private final StandaloneWhereFinisher whereBuilder = new StandaloneWhereFinisher(); - private WhereDSL() { - super(); + @Override + public StandaloneWhereFinisher where() { + return whereBuilder; } @Override - protected WhereDSL getThis() { + public WhereDSL configureStatement(Consumer consumer) { + consumer.accept(statementConfiguration); return this; } - public static WhereDSL where() { - return new WhereDSL(); - } + public class StandaloneWhereFinisher extends AbstractWhereFinisher + implements Buildable { + private StandaloneWhereFinisher() { + super(WhereDSL.this); + } - @Override - protected WhereModel buildWhereModel() { - return super.internalBuild(); - } + @Override + protected StandaloneWhereFinisher getThis() { + return this; + } + + @Override + public WhereModel build() { + return new WhereModel.Builder() + .withInitialCriterion(getInitialCriterion()) + .withSubCriteria(subCriteria) + .withStatementConfiguration(statementConfiguration) + .build(); + } - public WhereModel build() { - return buildWhereModel(); + public WhereApplier toWhereApplier() { + return d -> d.initialize(getInitialCriterion(), subCriteria); + } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/WhereModel.java b/src/main/java/org/mybatis/dynamic/sql/where/WhereModel.java index b37a6c00b..27fed4eed 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/WhereModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/WhereModel.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,84 +15,105 @@ */ package org.mybatis.dynamic.sql.where; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; -import java.util.stream.Stream; +import java.util.Objects; +import java.util.Optional; -import org.mybatis.dynamic.sql.SqlCriterion; +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionModel; +import org.mybatis.dynamic.sql.configuration.StatementConfiguration; +import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.render.RenderingStrategy; import org.mybatis.dynamic.sql.render.TableAliasCalculator; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.where.render.DefaultWhereClauseProvider; import org.mybatis.dynamic.sql.where.render.WhereClauseProvider; import org.mybatis.dynamic.sql.where.render.WhereRenderer; -public class WhereModel { - private static final WhereClauseProvider EMPTY_WHERE_CLAUSE = - new WhereClauseProvider.Builder().withWhereClause("").build(); //$NON-NLS-1$ - - private List> criteria = new ArrayList<>(); - - private WhereModel(List> criteria) { - this.criteria.addAll(criteria); - } - - public Stream mapCriteria(Function, R> mapper) { - return criteria.stream().map(mapper); +public class WhereModel extends AbstractBooleanExpressionModel { + private final StatementConfiguration statementConfiguration; + + private WhereModel(Builder builder) { + super(builder); + statementConfiguration = Objects.requireNonNull(builder.statementConfiguration); } /** * Renders a where clause without table aliases. - * - * @param renderingStrategy rendering strategy + * + * @param renderingStrategy + * rendering strategy + * * @return rendered where clause */ - public WhereClauseProvider render(RenderingStrategy renderingStrategy) { - return WhereRenderer.withWhereModel(this) - .withRenderingStrategy(renderingStrategy) - .withSequence(new AtomicInteger(1)) - .withTableAliasCalculator(TableAliasCalculator.empty()) - .build() - .render() - .orElse(EMPTY_WHERE_CLAUSE); + public Optional render(RenderingStrategy renderingStrategy) { + RenderingContext renderingContext = RenderingContext.withRenderingStrategy(renderingStrategy) + .withStatementConfiguration(statementConfiguration).build(); + + return render(renderingContext); } - - public WhereClauseProvider render(RenderingStrategy renderingStrategy, - TableAliasCalculator tableAliasCalculator) { - return WhereRenderer.withWhereModel(this) + + public Optional render(RenderingStrategy renderingStrategy, + TableAliasCalculator tableAliasCalculator) { + RenderingContext renderingContext = RenderingContext .withRenderingStrategy(renderingStrategy) - .withSequence(new AtomicInteger(1)) .withTableAliasCalculator(tableAliasCalculator) - .build() - .render() - .orElse(EMPTY_WHERE_CLAUSE); + .withStatementConfiguration(statementConfiguration) + .build(); + + return render(renderingContext); } - - public WhereClauseProvider render(RenderingStrategy renderingStrategy, - String parameterName) { - return WhereRenderer.withWhereModel(this) + + public Optional render(RenderingStrategy renderingStrategy, String parameterName) { + RenderingContext renderingContext = RenderingContext .withRenderingStrategy(renderingStrategy) - .withSequence(new AtomicInteger(1)) - .withTableAliasCalculator(TableAliasCalculator.empty()) .withParameterName(parameterName) - .build() - .render() - .orElse(EMPTY_WHERE_CLAUSE); + .withStatementConfiguration(statementConfiguration) + .build(); + + return render(renderingContext); } - - public WhereClauseProvider render(RenderingStrategy renderingStrategy, + + public Optional render(RenderingStrategy renderingStrategy, TableAliasCalculator tableAliasCalculator, String parameterName) { - return WhereRenderer.withWhereModel(this) + RenderingContext renderingContext = RenderingContext .withRenderingStrategy(renderingStrategy) - .withSequence(new AtomicInteger(1)) .withTableAliasCalculator(tableAliasCalculator) .withParameterName(parameterName) + .withStatementConfiguration(statementConfiguration) + .build(); + + return render(renderingContext); + } + + private Optional render(RenderingContext renderingContext) { + return WhereRenderer.withWhereModel(this) + .withRenderingContext(renderingContext) .build() .render() - .orElse(EMPTY_WHERE_CLAUSE); + .map(this::toWhereClauseProvider); + } + + private WhereClauseProvider toWhereClauseProvider(FragmentAndParameters fragmentAndParameters) { + return DefaultWhereClauseProvider.withWhereClause(fragmentAndParameters.fragment()) + .withParameters(fragmentAndParameters.parameters()) + .build(); } - - public static WhereModel of(List> criteria) { - return new WhereModel(criteria); + + public static class Builder extends AbstractBuilder { + private @Nullable StatementConfiguration statementConfiguration; + + public Builder withStatementConfiguration(StatementConfiguration statementConfiguration) { + this.statementConfiguration = statementConfiguration; + return this; + } + + public WhereModel build() { + return new WhereModel(this); + } + + @Override + protected Builder getThis() { + return this; + } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/AndGatherer.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/AndGatherer.java index 0903767f1..8a587262a 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/AndGatherer.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/AndGatherer.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -18,30 +18,29 @@ import java.util.function.Supplier; /** - * Utility class supporting the "and" part of a between condition. This class supports builders, - * so it is mutable. - * + * Utility class supporting the "and" part of a between condition. This class supports builders, so it is mutable. + * * @author Jeff Butler * - * @param the type of field for the between condition - * @param the type of condition being built + * @param + * the type of field for the between condition + * @param + * the type of condition being built */ public abstract class AndGatherer { - protected Supplier valueSupplier1; - protected Supplier valueSupplier2; - - protected AndGatherer(Supplier valueSupplier1) { - this.valueSupplier1 = valueSupplier1; + protected final T value1; + + protected AndGatherer(T value1) { + this.value1 = value1; } - + public R and(T value2) { - return and(() -> value2); + return build(value2); } public R and(Supplier valueSupplier2) { - this.valueSupplier2 = valueSupplier2; - return build(); + return and(valueSupplier2.get()); } - - protected abstract R build(); + + protected abstract R build(T value2); } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/AndWhenPresentGatherer.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/AndWhenPresentGatherer.java new file mode 100644 index 000000000..d4d8f3d7c --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/AndWhenPresentGatherer.java @@ -0,0 +1,49 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.where.condition; + +import java.util.function.Supplier; + +import org.jspecify.annotations.Nullable; + +/** + * Utility class supporting the "and" part of a between when present condition. This class supports builders, + * so it is mutable. + * + * @author Jeff Butler + * + * @param + * the type of field for the between condition + * @param + * the type of condition being built + */ +public abstract class AndWhenPresentGatherer { + protected final @Nullable T value1; + + protected AndWhenPresentGatherer(@Nullable T value1) { + this.value1 = value1; + } + + public R and(@Nullable T value2) { + return build(value2); + } + + public R and(Supplier<@Nullable T> valueSupplier2) { + return and(valueSupplier2.get()); + } + + protected abstract R build(@Nullable T value2); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/CaseInsensitiveRenderableCondition.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/CaseInsensitiveRenderableCondition.java new file mode 100644 index 000000000..977a0090b --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/CaseInsensitiveRenderableCondition.java @@ -0,0 +1,31 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.where.condition; + +import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.RenderableCondition; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + +public interface CaseInsensitiveRenderableCondition extends RenderableCondition { + + @Override + default FragmentAndParameters renderLeftColumn(RenderingContext renderingContext, + BindableColumn leftColumn) { + return RenderableCondition.super.renderLeftColumn(renderingContext, leftColumn) + .mapFragment(s -> "upper(" + s + ")"); //$NON-NLS-1$ //$NON-NLS-2$ + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsBetween.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsBetween.java index a404df544..ffc801508 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsBetween.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsBetween.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,48 +15,85 @@ */ package org.mybatis.dynamic.sql.where.condition; +import java.util.NoSuchElementException; import java.util.function.BiPredicate; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; +import java.util.function.Function; +import java.util.function.Predicate; import org.mybatis.dynamic.sql.AbstractTwoValueCondition; -public class IsBetween extends AbstractTwoValueCondition { +public class IsBetween extends AbstractTwoValueCondition + implements AbstractTwoValueCondition.Filterable, AbstractTwoValueCondition.Mappable { + private static final IsBetween EMPTY = new IsBetween(-1, -1) { + @Override + public Object value1() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } + + @Override + public Object value2() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } + + @Override + public boolean isEmpty() { + return true; + } + }; - protected IsBetween(Supplier valueSupplier1, Supplier valueSupplier2) { - super(valueSupplier1, valueSupplier2); + public static IsBetween empty() { + @SuppressWarnings("unchecked") + IsBetween t = (IsBetween) EMPTY; + return t; } - - protected IsBetween(Supplier valueSupplier1, Supplier valueSupplier2, BiPredicate predicate) { - super(valueSupplier1, valueSupplier2, predicate); + + protected IsBetween(T value1, T value2) { + super(value1, value2); } - + @Override - public String renderCondition(String columnName, String placeholder1, String placeholder2) { - return columnName + " between " + placeholder1 + " and " + placeholder2; //$NON-NLS-1$ //$NON-NLS-2$ + public String operator1() { + return "between"; //$NON-NLS-1$ } - public static class Builder extends AndGatherer> { - private Builder(Supplier valueSupplier1) { - super(valueSupplier1); - } - - @Override - protected IsBetween build() { - return new IsBetween<>(valueSupplier1, valueSupplier2); - } + @Override + public String operator2() { + return "and"; //$NON-NLS-1$ } - - public static Builder isBetween(Supplier valueSupplier1) { - return new Builder<>(valueSupplier1); + + @Override + public IsBetween filter(BiPredicate predicate) { + return filterSupport(predicate, IsBetween::empty, this); + } + + @Override + public IsBetween filter(Predicate predicate) { + return filterSupport(predicate, IsBetween::empty, this); } - public IsBetween when(BiPredicate predicate) { - return new IsBetween<>(valueSupplier1, valueSupplier2, predicate); + @Override + public IsBetween map(Function mapper1, + Function mapper2) { + return mapSupport(mapper1, mapper2, IsBetween::new, IsBetween::empty); + } + + @Override + public IsBetween map(Function mapper) { + return map(mapper, mapper); } - public IsBetween then(UnaryOperator transformer1, UnaryOperator transformer2) { - return shouldRender() ? new IsBetween<>(() -> transformer1.apply(value1()), - () -> transformer2.apply(value2())) : this; + public static Builder isBetween(T value1) { + return new Builder<>(value1); + } + + public static class Builder extends AndGatherer> { + private Builder(T value1) { + super(value1); + } + + @Override + protected IsBetween build(T value2) { + return new IsBetween<>(value1, value2); + } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsBetweenWhenPresent.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsBetweenWhenPresent.java index 117ca74a7..7b09bcb44 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsBetweenWhenPresent.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsBetweenWhenPresent.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,35 +15,94 @@ */ package org.mybatis.dynamic.sql.where.condition; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; +import java.util.NoSuchElementException; +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.function.Predicate; -import org.mybatis.dynamic.sql.util.Predicates; +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.AbstractTwoValueCondition; -public class IsBetweenWhenPresent extends IsBetween { +public class IsBetweenWhenPresent extends AbstractTwoValueCondition + implements AbstractTwoValueCondition.Filterable, AbstractTwoValueCondition.Mappable { + private static final IsBetweenWhenPresent EMPTY = new IsBetweenWhenPresent(-1, -1) { + @Override + public Object value1() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } - protected IsBetweenWhenPresent(Supplier valueSupplier1, Supplier valueSupplier2) { - super(valueSupplier1, valueSupplier2, Predicates.bothPresent()); - } - - public static class Builder extends AndGatherer> { - private Builder(Supplier valueSupplier1) { - super(valueSupplier1); + @Override + public Object value2() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ } - + @Override - protected IsBetweenWhenPresent build() { - return new IsBetweenWhenPresent<>(valueSupplier1, valueSupplier2); + public boolean isEmpty() { + return true; } + }; + + public static IsBetweenWhenPresent empty() { + @SuppressWarnings("unchecked") + IsBetweenWhenPresent t = (IsBetweenWhenPresent) EMPTY; + return t; } - - public static Builder isBetweenWhenPresent(Supplier valueSupplier) { - return new Builder<>(valueSupplier); + + protected IsBetweenWhenPresent(T value1, T value2) { + super(value1, value2); } @Override - public IsBetweenWhenPresent then(UnaryOperator transformer1, UnaryOperator transformer2) { - return shouldRender() ? new IsBetweenWhenPresent<>(() -> transformer1.apply(value1()), - () -> transformer2.apply(value2())) : this; + public String operator1() { + return "between"; //$NON-NLS-1$ + } + + @Override + public String operator2() { + return "and"; //$NON-NLS-1$ + } + + @Override + public IsBetweenWhenPresent filter(BiPredicate predicate) { + return filterSupport(predicate, IsBetweenWhenPresent::empty, this); + } + + @Override + public IsBetweenWhenPresent filter(Predicate predicate) { + return filterSupport(predicate, IsBetweenWhenPresent::empty, this); + } + + @Override + public IsBetweenWhenPresent map(Function mapper1, + Function mapper2) { + return mapSupport(mapper1, mapper2, IsBetweenWhenPresent::of, IsBetweenWhenPresent::empty); + } + + @Override + public IsBetweenWhenPresent map(Function mapper) { + return map(mapper, mapper); + } + + public static IsBetweenWhenPresent of(@Nullable T value1, @Nullable T value2) { + if (value1 == null || value2 == null) { + return empty(); + } else { + return new IsBetweenWhenPresent<>(value1, value2); + } + } + + public static Builder isBetweenWhenPresent(@Nullable T value1) { + return new Builder<>(value1); + } + + public static class Builder extends AndWhenPresentGatherer> { + private Builder(@Nullable T value1) { + super(value1); + } + + @Override + protected IsBetweenWhenPresent build(@Nullable T value2) { + return of(value1, value2); + } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsEqualTo.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsEqualTo.java index 4235b1a86..51e6e4d47 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsEqualTo.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsEqualTo.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,36 +15,53 @@ */ package org.mybatis.dynamic.sql.where.condition; +import java.util.NoSuchElementException; +import java.util.function.Function; import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; import org.mybatis.dynamic.sql.AbstractSingleValueCondition; -public class IsEqualTo extends AbstractSingleValueCondition { +public class IsEqualTo extends AbstractSingleValueCondition + implements AbstractSingleValueCondition.Filterable, AbstractSingleValueCondition.Mappable { - protected IsEqualTo(Supplier valueSupplier) { - super(valueSupplier); + private static final IsEqualTo EMPTY = new IsEqualTo(-1) { + @Override + public Object value() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } + + @Override + public boolean isEmpty() { + return true; + } + }; + + public static IsEqualTo empty() { + @SuppressWarnings("unchecked") + IsEqualTo t = (IsEqualTo) EMPTY; + return t; } - protected IsEqualTo(Supplier valueSupplier, Predicate predicate) { - super(valueSupplier, predicate); + protected IsEqualTo(T value) { + super(value); } @Override - public String renderCondition(String columnName, String placeholder) { - return columnName + " = " + placeholder; //$NON-NLS-1$ + public String operator() { + return "="; //$NON-NLS-1$ } - - public static IsEqualTo of(Supplier valueSupplier) { - return new IsEqualTo<>(valueSupplier); + + public static IsEqualTo of(T value) { + return new IsEqualTo<>(value); } - - public IsEqualTo when(Predicate predicate) { - return new IsEqualTo<>(valueSupplier, predicate); + + @Override + public IsEqualTo filter(Predicate predicate) { + return filterSupport(predicate, IsEqualTo::empty, this); } - public IsEqualTo then(UnaryOperator transformer) { - return shouldRender() ? new IsEqualTo<>(() -> transformer.apply(value())) : this; + @Override + public IsEqualTo map(Function mapper) { + return mapSupport(mapper, IsEqualTo::new, IsEqualTo::empty); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsEqualToColumn.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsEqualToColumn.java index 36df149a5..6f12ea99f 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsEqualToColumn.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsEqualToColumn.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -25,10 +25,10 @@ protected IsEqualToColumn(BasicColumn column) { } @Override - protected String renderCondition(String leftColumn, String rightColumn) { - return leftColumn + " = " + rightColumn; //$NON-NLS-1$ + public String operator() { + return "="; //$NON-NLS-1$ } - + public static IsEqualToColumn of(BasicColumn column) { return new IsEqualToColumn<>(column); } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsEqualToWhenPresent.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsEqualToWhenPresent.java index 12af85b9b..f06489076 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsEqualToWhenPresent.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsEqualToWhenPresent.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,22 +15,58 @@ */ package org.mybatis.dynamic.sql.where.condition; -import java.util.Objects; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; +import java.util.NoSuchElementException; +import java.util.function.Function; +import java.util.function.Predicate; -public class IsEqualToWhenPresent extends IsEqualTo { +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.AbstractSingleValueCondition; - protected IsEqualToWhenPresent(Supplier valueSupplier) { - super(valueSupplier, Objects::nonNull); +public class IsEqualToWhenPresent extends AbstractSingleValueCondition + implements AbstractSingleValueCondition.Filterable, AbstractSingleValueCondition.Mappable { + + private static final IsEqualToWhenPresent EMPTY = new IsEqualToWhenPresent(-1) { + @Override + public Object value() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } + + @Override + public boolean isEmpty() { + return true; + } + }; + + public static IsEqualToWhenPresent empty() { + @SuppressWarnings("unchecked") + IsEqualToWhenPresent t = (IsEqualToWhenPresent) EMPTY; + return t; } - public static IsEqualToWhenPresent of(Supplier valueSupplier) { - return new IsEqualToWhenPresent<>(valueSupplier); + protected IsEqualToWhenPresent(T value) { + super(value); + } + + @Override + public String operator() { + return "="; //$NON-NLS-1$ + } + + public static IsEqualToWhenPresent of(@Nullable T value) { + if (value == null) { + return empty(); + } else { + return new IsEqualToWhenPresent<>(value); + } + } + + @Override + public IsEqualToWhenPresent filter(Predicate predicate) { + return filterSupport(predicate, IsEqualToWhenPresent::empty, this); } @Override - public IsEqualToWhenPresent then(UnaryOperator transformer) { - return shouldRender() ? new IsEqualToWhenPresent<>(() -> transformer.apply(value())) : this; + public IsEqualToWhenPresent map(Function mapper) { + return mapSupport(mapper, IsEqualToWhenPresent::of, IsEqualToWhenPresent::empty); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsEqualToWithSubselect.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsEqualToWithSubselect.java index d6fdea54d..10bc2c285 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsEqualToWithSubselect.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsEqualToWithSubselect.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -20,7 +20,7 @@ import org.mybatis.dynamic.sql.util.Buildable; public class IsEqualToWithSubselect extends AbstractSubselectCondition { - + protected IsEqualToWithSubselect(Buildable selectModelBuilder) { super(selectModelBuilder); } @@ -30,7 +30,7 @@ public static IsEqualToWithSubselect of(Buildable selectMode } @Override - public String renderCondition(String columnName, String renderedSelectStatement) { - return columnName + " = (" + renderedSelectStatement + ")"; //$NON-NLS-1$ //$NON-NLS-2$ + public String operator() { + return "="; //$NON-NLS-1$ } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThan.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThan.java index 2909e6e39..93be70911 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThan.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThan.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,36 +15,52 @@ */ package org.mybatis.dynamic.sql.where.condition; +import java.util.NoSuchElementException; +import java.util.function.Function; import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; import org.mybatis.dynamic.sql.AbstractSingleValueCondition; -public class IsGreaterThan extends AbstractSingleValueCondition { +public class IsGreaterThan extends AbstractSingleValueCondition + implements AbstractSingleValueCondition.Filterable, AbstractSingleValueCondition.Mappable { + private static final IsGreaterThan EMPTY = new IsGreaterThan(-1) { + @Override + public Object value() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } - protected IsGreaterThan(Supplier valueSupplier) { - super(valueSupplier); + @Override + public boolean isEmpty() { + return true; + } + }; + + public static IsGreaterThan empty() { + @SuppressWarnings("unchecked") + IsGreaterThan t = (IsGreaterThan) EMPTY; + return t; } - - protected IsGreaterThan(Supplier valueSupplier, Predicate predicate) { - super(valueSupplier, predicate); + + protected IsGreaterThan(T value) { + super(value); } - + @Override - public String renderCondition(String columnName, String placeholder) { - return columnName + " > " + placeholder; //$NON-NLS-1$ + public String operator() { + return ">"; //$NON-NLS-1$ } - public static IsGreaterThan of(Supplier valueSupplier) { - return new IsGreaterThan<>(valueSupplier); + public static IsGreaterThan of(T value) { + return new IsGreaterThan<>(value); } - - public IsGreaterThan when(Predicate predicate) { - return new IsGreaterThan<>(valueSupplier, predicate); + + @Override + public IsGreaterThan filter(Predicate predicate) { + return filterSupport(predicate, IsGreaterThan::empty, this); } - public IsGreaterThan then(UnaryOperator transformer) { - return shouldRender() ? new IsGreaterThan<>(() -> transformer.apply(value())) : this; + @Override + public IsGreaterThan map(Function mapper) { + return mapSupport(mapper, IsGreaterThan::new, IsGreaterThan::empty); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanColumn.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanColumn.java index f3d7bcab4..446ab6377 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanColumn.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanColumn.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -25,10 +25,10 @@ protected IsGreaterThanColumn(BasicColumn column) { } @Override - protected String renderCondition(String leftColumn, String rightColumn) { - return leftColumn + " > " + rightColumn; //$NON-NLS-1$ + public String operator() { + return ">"; //$NON-NLS-1$ } - + public static IsGreaterThanColumn of(BasicColumn column) { return new IsGreaterThanColumn<>(column); } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanOrEqualTo.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanOrEqualTo.java index 569a68db8..8373bf352 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanOrEqualTo.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanOrEqualTo.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,36 +15,52 @@ */ package org.mybatis.dynamic.sql.where.condition; +import java.util.NoSuchElementException; +import java.util.function.Function; import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; import org.mybatis.dynamic.sql.AbstractSingleValueCondition; -public class IsGreaterThanOrEqualTo extends AbstractSingleValueCondition { +public class IsGreaterThanOrEqualTo extends AbstractSingleValueCondition + implements AbstractSingleValueCondition.Filterable, AbstractSingleValueCondition.Mappable { + private static final IsGreaterThanOrEqualTo EMPTY = new IsGreaterThanOrEqualTo(-1) { + @Override + public Object value() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } - protected IsGreaterThanOrEqualTo(Supplier valueSupplier) { - super(valueSupplier); + @Override + public boolean isEmpty() { + return true; + } + }; + + public static IsGreaterThanOrEqualTo empty() { + @SuppressWarnings("unchecked") + IsGreaterThanOrEqualTo t = (IsGreaterThanOrEqualTo) EMPTY; + return t; } - - protected IsGreaterThanOrEqualTo(Supplier valueSupplier, Predicate predicate) { - super(valueSupplier, predicate); + + protected IsGreaterThanOrEqualTo(T value) { + super(value); } - + @Override - public String renderCondition(String columnName, String placeholder) { - return columnName + " >= " + placeholder; //$NON-NLS-1$ + public String operator() { + return ">="; //$NON-NLS-1$ } - public static IsGreaterThanOrEqualTo of(Supplier valueSupplier) { - return new IsGreaterThanOrEqualTo<>(valueSupplier); + public static IsGreaterThanOrEqualTo of(T value) { + return new IsGreaterThanOrEqualTo<>(value); } - - public IsGreaterThanOrEqualTo when(Predicate predicate) { - return new IsGreaterThanOrEqualTo<>(valueSupplier, predicate); + + @Override + public IsGreaterThanOrEqualTo filter(Predicate predicate) { + return filterSupport(predicate, IsGreaterThanOrEqualTo::empty, this); } - public IsGreaterThanOrEqualTo then(UnaryOperator transformer) { - return shouldRender() ? new IsGreaterThanOrEqualTo<>(() -> transformer.apply(value())) : this; + @Override + public IsGreaterThanOrEqualTo map(Function mapper) { + return mapSupport(mapper, IsGreaterThanOrEqualTo::new, IsGreaterThanOrEqualTo::empty); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanOrEqualToColumn.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanOrEqualToColumn.java index 2f4ab4642..3b8d4a05e 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanOrEqualToColumn.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanOrEqualToColumn.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -25,10 +25,10 @@ protected IsGreaterThanOrEqualToColumn(BasicColumn column) { } @Override - protected String renderCondition(String leftColumn, String rightColumn) { - return leftColumn + " >= " + rightColumn; //$NON-NLS-1$ + public String operator() { + return ">="; //$NON-NLS-1$ } - + public static IsGreaterThanOrEqualToColumn of(BasicColumn column) { return new IsGreaterThanOrEqualToColumn<>(column); } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanOrEqualToWhenPresent.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanOrEqualToWhenPresent.java index 495da0ae2..970ef0775 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanOrEqualToWhenPresent.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanOrEqualToWhenPresent.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,22 +15,58 @@ */ package org.mybatis.dynamic.sql.where.condition; -import java.util.Objects; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; +import java.util.NoSuchElementException; +import java.util.function.Function; +import java.util.function.Predicate; -public class IsGreaterThanOrEqualToWhenPresent extends IsGreaterThanOrEqualTo { +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.AbstractSingleValueCondition; - protected IsGreaterThanOrEqualToWhenPresent(Supplier valueSupplier) { - super(valueSupplier, Objects::nonNull); +public class IsGreaterThanOrEqualToWhenPresent extends AbstractSingleValueCondition + implements AbstractSingleValueCondition.Filterable, AbstractSingleValueCondition.Mappable { + private static final IsGreaterThanOrEqualToWhenPresent EMPTY = + new IsGreaterThanOrEqualToWhenPresent(-1) { + @Override + public Object value() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } + + @Override + public boolean isEmpty() { + return true; + } + }; + + public static IsGreaterThanOrEqualToWhenPresent empty() { + @SuppressWarnings("unchecked") + IsGreaterThanOrEqualToWhenPresent t = (IsGreaterThanOrEqualToWhenPresent) EMPTY; + return t; + } + + protected IsGreaterThanOrEqualToWhenPresent(T value) { + super(value); } - - public static IsGreaterThanOrEqualToWhenPresent of(Supplier valueSupplier) { - return new IsGreaterThanOrEqualToWhenPresent<>(valueSupplier); + + @Override + public String operator() { + return ">="; //$NON-NLS-1$ + } + + public static IsGreaterThanOrEqualToWhenPresent of(@Nullable T value) { + if (value == null) { + return empty(); + } else { + return new IsGreaterThanOrEqualToWhenPresent<>(value); + } + } + + @Override + public IsGreaterThanOrEqualToWhenPresent filter(Predicate predicate) { + return filterSupport(predicate, IsGreaterThanOrEqualToWhenPresent::empty, this); } @Override - public IsGreaterThanOrEqualToWhenPresent then(UnaryOperator transformer) { - return shouldRender() ? new IsGreaterThanOrEqualToWhenPresent<>(() -> transformer.apply(value())) : this; + public IsGreaterThanOrEqualToWhenPresent map(Function mapper) { + return mapSupport(mapper, IsGreaterThanOrEqualToWhenPresent::of, IsGreaterThanOrEqualToWhenPresent::empty); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanOrEqualToWithSubselect.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanOrEqualToWithSubselect.java index 56d571ab7..05aea23f2 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanOrEqualToWithSubselect.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanOrEqualToWithSubselect.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -20,7 +20,7 @@ import org.mybatis.dynamic.sql.util.Buildable; public class IsGreaterThanOrEqualToWithSubselect extends AbstractSubselectCondition { - + protected IsGreaterThanOrEqualToWithSubselect(Buildable selectModelBuilder) { super(selectModelBuilder); } @@ -30,7 +30,7 @@ public static IsGreaterThanOrEqualToWithSubselect of(Buildable= (" + renderedSelectStatement + ")"; //$NON-NLS-1$ //$NON-NLS-2$ + public String operator() { + return ">="; //$NON-NLS-1$ } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanWhenPresent.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanWhenPresent.java index f06fd3901..175b5fcf6 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanWhenPresent.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanWhenPresent.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,22 +15,57 @@ */ package org.mybatis.dynamic.sql.where.condition; -import java.util.Objects; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; +import java.util.NoSuchElementException; +import java.util.function.Function; +import java.util.function.Predicate; -public class IsGreaterThanWhenPresent extends IsGreaterThan { +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.AbstractSingleValueCondition; - protected IsGreaterThanWhenPresent(Supplier valueSupplier) { - super(valueSupplier, Objects::nonNull); +public class IsGreaterThanWhenPresent extends AbstractSingleValueCondition + implements AbstractSingleValueCondition.Filterable, AbstractSingleValueCondition.Mappable { + private static final IsGreaterThanWhenPresent EMPTY = new IsGreaterThanWhenPresent(-1) { + @Override + public Object value() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } + + @Override + public boolean isEmpty() { + return true; + } + }; + + public static IsGreaterThanWhenPresent empty() { + @SuppressWarnings("unchecked") + IsGreaterThanWhenPresent t = (IsGreaterThanWhenPresent) EMPTY; + return t; + } + + protected IsGreaterThanWhenPresent(T value) { + super(value); } - - public static IsGreaterThanWhenPresent of(Supplier valueSupplier) { - return new IsGreaterThanWhenPresent<>(valueSupplier); + + @Override + public String operator() { + return ">"; //$NON-NLS-1$ + } + + public static IsGreaterThanWhenPresent of(@Nullable T value) { + if (value == null) { + return empty(); + } else { + return new IsGreaterThanWhenPresent<>(value); + } + } + + @Override + public IsGreaterThanWhenPresent filter(Predicate predicate) { + return filterSupport(predicate, IsGreaterThanWhenPresent::empty, this); } @Override - public IsGreaterThanWhenPresent then(UnaryOperator transformer) { - return shouldRender() ? new IsGreaterThanWhenPresent<>(() -> transformer.apply(value())) : this; + public IsGreaterThanWhenPresent map(Function mapper) { + return mapSupport(mapper, IsGreaterThanWhenPresent::of, IsGreaterThanWhenPresent::empty); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanWithSubselect.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanWithSubselect.java index 34421ecd6..225423cbd 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanWithSubselect.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanWithSubselect.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -20,7 +20,7 @@ import org.mybatis.dynamic.sql.util.Buildable; public class IsGreaterThanWithSubselect extends AbstractSubselectCondition { - + protected IsGreaterThanWithSubselect(Buildable selectModelBuilder) { super(selectModelBuilder); } @@ -30,7 +30,7 @@ public static IsGreaterThanWithSubselect of(Buildable select } @Override - public String renderCondition(String columnName, String renderedSelectStatement) { - return columnName + " > (" + renderedSelectStatement + ")"; //$NON-NLS-1$ //$NON-NLS-2$ + public String operator() { + return ">"; //$NON-NLS-1$ } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsIn.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsIn.java index df6c48bc3..098db45f1 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsIn.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsIn.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,19 +15,24 @@ */ package org.mybatis.dynamic.sql.where.condition; -import static org.mybatis.dynamic.sql.util.StringUtilities.spaceAfter; - +import java.util.Arrays; import java.util.Collection; -import java.util.function.UnaryOperator; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import java.util.Collections; +import java.util.function.Function; +import java.util.function.Predicate; import org.mybatis.dynamic.sql.AbstractListValueCondition; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.Validator; -public class IsIn extends AbstractListValueCondition { +public class IsIn extends AbstractListValueCondition + implements AbstractListValueCondition.Filterable, AbstractListValueCondition.Mappable { + private static final IsIn EMPTY = new IsIn<>(Collections.emptyList()); - protected IsIn(Collection values, UnaryOperator> valueStreamTransformer) { - super(values, valueStreamTransformer); + public static IsIn empty() { + @SuppressWarnings("unchecked") + IsIn t = (IsIn) EMPTY; + return t; } protected IsIn(Collection values) { @@ -35,23 +40,29 @@ protected IsIn(Collection values) { } @Override - public String renderCondition(String columnName, Stream placeholders) { - return spaceAfter(columnName) - + placeholders.collect(Collectors.joining(",", "in (", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - } - - /** - * This method allows you to modify the condition's values before they are placed into the parameter map. - * For example, you could filter nulls, or trim strings, etc. This process will run before final rendering of SQL. - * If you filter values out of the stream, then final condition will not reference those values. If you filter all - * values out of the stream, then the condition will not render. - * - * @param valueStreamTransformer a UnaryOperator that will transform the value stream before - * the values are placed in the parameter map - * @return new condition with the specified transformer - */ - public IsIn then(UnaryOperator> valueStreamTransformer) { - return new IsIn<>(values, valueStreamTransformer); + public boolean shouldRender(RenderingContext renderingContext) { + Validator.assertNotEmpty(values, "ERROR.44", "IsIn"); //$NON-NLS-1$ //$NON-NLS-2$ + return true; + } + + @Override + public String operator() { + return "in"; //$NON-NLS-1$ + } + + @Override + public IsIn filter(Predicate predicate) { + return filterSupport(predicate, IsIn::new, this, IsIn::empty); + } + + @Override + public IsIn map(Function mapper) { + return mapSupport(mapper, IsIn::new, IsIn::empty); + } + + @SafeVarargs + public static IsIn of(T... values) { + return of(Arrays.asList(values)); } public static IsIn of(Collection values) { diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInCaseInsensitive.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInCaseInsensitive.java index 622a6ea40..3e2a22d2a 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInCaseInsensitive.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInCaseInsensitive.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,45 +15,59 @@ */ package org.mybatis.dynamic.sql.where.condition; +import java.util.Arrays; import java.util.Collection; -import java.util.function.UnaryOperator; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import java.util.Collections; +import java.util.function.Function; +import java.util.function.Predicate; import org.mybatis.dynamic.sql.AbstractListValueCondition; +import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.util.StringUtilities; +import org.mybatis.dynamic.sql.util.Validator; -public class IsInCaseInsensitive extends AbstractListValueCondition { +public class IsInCaseInsensitive extends AbstractListValueCondition + implements CaseInsensitiveRenderableCondition, AbstractListValueCondition.Filterable, + AbstractListValueCondition.Mappable { + private static final IsInCaseInsensitive EMPTY = new IsInCaseInsensitive<>(Collections.emptyList()); - protected IsInCaseInsensitive(Collection values) { - super(values, s -> s.map(StringUtilities::safelyUpperCase)); + public static IsInCaseInsensitive empty() { + @SuppressWarnings("unchecked") + IsInCaseInsensitive t = (IsInCaseInsensitive) EMPTY; + return t; } - protected IsInCaseInsensitive(Collection values, UnaryOperator> valueStreamTransformer) { - super(values, StringUtilities.upperCaseAfter(valueStreamTransformer)); + protected IsInCaseInsensitive(Collection values) { + super(values.stream().map(StringUtilities::upperCaseIfPossible).toList()); } @Override - public String renderCondition(String columnName, Stream placeholders) { - return "upper(" + columnName + ") " + //$NON-NLS-1$ //$NON-NLS-2$ - placeholders.collect(Collectors.joining(",", "in (", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - } - - /** - * This method allows you to modify the condition's values before they are placed into the parameter map. - * For example, you could filter nulls, or trim strings, etc. This process will run before final rendering of SQL. - * If you filter values out of the stream, then final condition will not reference those values. If you filter all - * values out of the stream, then the condition will not render. - * - * @param valueStreamTransformer a UnaryOperator that will transform the value stream before - * the values are placed in the parameter map - * @return new condition with the specified transformer - */ - public IsInCaseInsensitive then(UnaryOperator> valueStreamTransformer) { - return new IsInCaseInsensitive(values, valueStreamTransformer); - } - - public static IsInCaseInsensitive of(Collection values) { - return new IsInCaseInsensitive(values); + public boolean shouldRender(RenderingContext renderingContext) { + Validator.assertNotEmpty(values, "ERROR.44", "IsInCaseInsensitive"); //$NON-NLS-1$ //$NON-NLS-2$ + return true; + } + + @Override + public String operator() { + return "in"; //$NON-NLS-1$ + } + + @Override + public IsInCaseInsensitive filter(Predicate predicate) { + return filterSupport(predicate, IsInCaseInsensitive::new, this, IsInCaseInsensitive::empty); + } + + @Override + public IsInCaseInsensitive map(Function mapper) { + return mapSupport(mapper, IsInCaseInsensitive::new, IsInCaseInsensitive::empty); + } + + @SafeVarargs + public static IsInCaseInsensitive of(T... values) { + return of(Arrays.asList(values)); + } + + public static IsInCaseInsensitive of(Collection values) { + return new IsInCaseInsensitive<>(values); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInCaseInsensitiveWhenPresent.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInCaseInsensitiveWhenPresent.java index 90c70c657..3400a8337 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInCaseInsensitiveWhenPresent.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInCaseInsensitiveWhenPresent.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,16 +15,59 @@ */ package org.mybatis.dynamic.sql.where.condition; +import java.util.Arrays; import java.util.Collection; -import java.util.Objects; +import java.util.Collections; +import java.util.function.Function; +import java.util.function.Predicate; -public class IsInCaseInsensitiveWhenPresent extends IsInCaseInsensitive { +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.AbstractListValueCondition; +import org.mybatis.dynamic.sql.util.StringUtilities; +import org.mybatis.dynamic.sql.util.Utilities; - protected IsInCaseInsensitiveWhenPresent(Collection values) { - super(values, s -> s.filter(Objects::nonNull)); +public class IsInCaseInsensitiveWhenPresent extends AbstractListValueCondition + implements CaseInsensitiveRenderableCondition, AbstractListValueCondition.Filterable, + AbstractListValueCondition.Mappable { + private static final IsInCaseInsensitiveWhenPresent EMPTY = + new IsInCaseInsensitiveWhenPresent<>(Collections.emptyList()); + + public static IsInCaseInsensitiveWhenPresent empty() { + @SuppressWarnings("unchecked") + IsInCaseInsensitiveWhenPresent t = (IsInCaseInsensitiveWhenPresent) EMPTY; + return t; + } + + protected IsInCaseInsensitiveWhenPresent(Collection values) { + super(Utilities.filterNulls(values).map(StringUtilities::upperCaseIfPossible).toList()); + } + + @Override + public String operator() { + return "in"; //$NON-NLS-1$ + } + + @Override + public IsInCaseInsensitiveWhenPresent filter(Predicate predicate) { + return filterSupport(predicate, IsInCaseInsensitiveWhenPresent::new, this, + IsInCaseInsensitiveWhenPresent::empty); + } + + @Override + public IsInCaseInsensitiveWhenPresent map(Function mapper) { + return mapSupport(mapper, IsInCaseInsensitiveWhenPresent::new, IsInCaseInsensitiveWhenPresent::empty); + } + + @SafeVarargs + public static IsInCaseInsensitiveWhenPresent of(@Nullable T... values) { + return of(Arrays.asList(values)); } - public static IsInCaseInsensitiveWhenPresent of(Collection values) { - return new IsInCaseInsensitiveWhenPresent(values); + public static IsInCaseInsensitiveWhenPresent of(@Nullable Collection<@Nullable T> values) { + if (values == null) { + return empty(); + } else { + return new IsInCaseInsensitiveWhenPresent<>(values); + } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInWhenPresent.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInWhenPresent.java index c4a61001e..246938da0 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInWhenPresent.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInWhenPresent.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,16 +15,55 @@ */ package org.mybatis.dynamic.sql.where.condition; +import java.util.Arrays; import java.util.Collection; -import java.util.Objects; +import java.util.Collections; +import java.util.function.Function; +import java.util.function.Predicate; -public class IsInWhenPresent extends IsIn { +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.AbstractListValueCondition; +import org.mybatis.dynamic.sql.util.Utilities; - protected IsInWhenPresent(Collection values) { - super(values, s -> s.filter(Objects::nonNull)); +public class IsInWhenPresent extends AbstractListValueCondition + implements AbstractListValueCondition.Filterable, AbstractListValueCondition.Mappable { + private static final IsInWhenPresent EMPTY = new IsInWhenPresent<>(Collections.emptyList()); + + public static IsInWhenPresent empty() { + @SuppressWarnings("unchecked") + IsInWhenPresent t = (IsInWhenPresent) EMPTY; + return t; + } + + protected IsInWhenPresent(Collection<@Nullable T> values) { + super(Utilities.filterNulls(values).toList()); + } + + @Override + public String operator() { + return "in"; //$NON-NLS-1$ + } + + @Override + public IsInWhenPresent filter(Predicate predicate) { + return filterSupport(predicate, IsInWhenPresent::new, this, IsInWhenPresent::empty); + } + + @Override + public IsInWhenPresent map(Function mapper) { + return mapSupport(mapper, IsInWhenPresent::of, IsInWhenPresent::empty); + } + + @SafeVarargs + public static IsInWhenPresent of(@Nullable T... values) { + return of(Arrays.asList(values)); } - public static IsInWhenPresent of(Collection values) { - return new IsInWhenPresent<>(values); + public static IsInWhenPresent of(@Nullable Collection<@Nullable T> values) { + if (values == null) { + return empty(); + } else { + return new IsInWhenPresent<>(values); + } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInWithSubselect.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInWithSubselect.java index 652e10032..771e2637b 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInWithSubselect.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInWithSubselect.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -20,7 +20,7 @@ import org.mybatis.dynamic.sql.util.Buildable; public class IsInWithSubselect extends AbstractSubselectCondition { - + protected IsInWithSubselect(Buildable selectModelBuilder) { super(selectModelBuilder); } @@ -30,7 +30,7 @@ public static IsInWithSubselect of(Buildable selectModelBuil } @Override - public String renderCondition(String columnName, String renderedSelectStatement) { - return columnName + " in (" + renderedSelectStatement + ")"; //$NON-NLS-1$ //$NON-NLS-2$ + public String operator() { + return "in"; //$NON-NLS-1$ } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThan.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThan.java index 0f0c243c5..3ed383fbd 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThan.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThan.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,36 +15,53 @@ */ package org.mybatis.dynamic.sql.where.condition; +import java.util.NoSuchElementException; +import java.util.function.Function; import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; import org.mybatis.dynamic.sql.AbstractSingleValueCondition; -public class IsLessThan extends AbstractSingleValueCondition { +public class IsLessThan extends AbstractSingleValueCondition + implements AbstractSingleValueCondition.Filterable, AbstractSingleValueCondition.Mappable { - protected IsLessThan(Supplier valueSupplier) { - super(valueSupplier); + private static final IsLessThan EMPTY = new IsLessThan(-1) { + @Override + public Object value() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } + + @Override + public boolean isEmpty() { + return true; + } + }; + + public static IsLessThan empty() { + @SuppressWarnings("unchecked") + IsLessThan t = (IsLessThan) EMPTY; + return t; } - - protected IsLessThan(Supplier valueSupplier, Predicate predicate) { - super(valueSupplier, predicate); + + protected IsLessThan(T value) { + super(value); } - + @Override - public String renderCondition(String columnName, String placeholder) { - return columnName + " < " + placeholder; //$NON-NLS-1$ + public String operator() { + return "<"; //$NON-NLS-1$ } - public static IsLessThan of(Supplier valueSupplier) { - return new IsLessThan<>(valueSupplier); + public static IsLessThan of(T value) { + return new IsLessThan<>(value); } - - public IsLessThan when(Predicate predicate) { - return new IsLessThan<>(valueSupplier, predicate); + + @Override + public IsLessThan filter(Predicate predicate) { + return filterSupport(predicate, IsLessThan::empty, this); } - public IsLessThan then(UnaryOperator transformer) { - return shouldRender() ? new IsLessThan<>(() -> transformer.apply(value())) : this; + @Override + public IsLessThan map(Function mapper) { + return mapSupport(mapper, IsLessThan::new, IsLessThan::empty); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanColumn.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanColumn.java index 13cebec88..d8fc2b0e7 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanColumn.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanColumn.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -25,10 +25,10 @@ protected IsLessThanColumn(BasicColumn column) { } @Override - protected String renderCondition(String leftColumn, String rightColumn) { - return leftColumn + " < " + rightColumn; //$NON-NLS-1$ + public String operator() { + return "<"; //$NON-NLS-1$ } - + public static IsLessThanColumn of(BasicColumn column) { return new IsLessThanColumn<>(column); } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanOrEqualTo.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanOrEqualTo.java index ccb5e4815..1b92e0c40 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanOrEqualTo.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanOrEqualTo.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,36 +15,52 @@ */ package org.mybatis.dynamic.sql.where.condition; +import java.util.NoSuchElementException; +import java.util.function.Function; import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; import org.mybatis.dynamic.sql.AbstractSingleValueCondition; -public class IsLessThanOrEqualTo extends AbstractSingleValueCondition { +public class IsLessThanOrEqualTo extends AbstractSingleValueCondition + implements AbstractSingleValueCondition.Filterable, AbstractSingleValueCondition.Mappable { + private static final IsLessThanOrEqualTo EMPTY = new IsLessThanOrEqualTo(-1) { + @Override + public Object value() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } - protected IsLessThanOrEqualTo(Supplier valueSupplier) { - super(valueSupplier); + @Override + public boolean isEmpty() { + return true; + } + }; + + public static IsLessThanOrEqualTo empty() { + @SuppressWarnings("unchecked") + IsLessThanOrEqualTo t = (IsLessThanOrEqualTo) EMPTY; + return t; } - - protected IsLessThanOrEqualTo(Supplier valueSupplier, Predicate predicate) { - super(valueSupplier, predicate); + + protected IsLessThanOrEqualTo(T value) { + super(value); } - + @Override - public String renderCondition(String columnName, String placeholder) { - return columnName + " <= " + placeholder; //$NON-NLS-1$ + public String operator() { + return "<="; //$NON-NLS-1$ } - public static IsLessThanOrEqualTo of(Supplier valueSupplier) { - return new IsLessThanOrEqualTo<>(valueSupplier); + public static IsLessThanOrEqualTo of(T value) { + return new IsLessThanOrEqualTo<>(value); } - - public IsLessThanOrEqualTo when(Predicate predicate) { - return new IsLessThanOrEqualTo<>(valueSupplier, predicate); + + @Override + public IsLessThanOrEqualTo filter(Predicate predicate) { + return filterSupport(predicate, IsLessThanOrEqualTo::empty, this); } - public IsLessThanOrEqualTo then(UnaryOperator transformer) { - return shouldRender() ? new IsLessThanOrEqualTo<>(() -> transformer.apply(value())) : this; + @Override + public IsLessThanOrEqualTo map(Function mapper) { + return mapSupport(mapper, IsLessThanOrEqualTo::new, IsLessThanOrEqualTo::empty); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanOrEqualToColumn.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanOrEqualToColumn.java index b387e6386..858f86c1f 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanOrEqualToColumn.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanOrEqualToColumn.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -25,10 +25,10 @@ protected IsLessThanOrEqualToColumn(BasicColumn column) { } @Override - protected String renderCondition(String leftColumn, String rightColumn) { - return leftColumn + " <= " + rightColumn; //$NON-NLS-1$ + public String operator() { + return "<="; //$NON-NLS-1$ } - + public static IsLessThanOrEqualToColumn of(BasicColumn column) { return new IsLessThanOrEqualToColumn<>(column); } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanOrEqualToWhenPresent.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanOrEqualToWhenPresent.java index eeb344d6f..3cdc8ff39 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanOrEqualToWhenPresent.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanOrEqualToWhenPresent.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,22 +15,57 @@ */ package org.mybatis.dynamic.sql.where.condition; -import java.util.Objects; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; +import java.util.NoSuchElementException; +import java.util.function.Function; +import java.util.function.Predicate; -public class IsLessThanOrEqualToWhenPresent extends IsLessThanOrEqualTo { +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.AbstractSingleValueCondition; - protected IsLessThanOrEqualToWhenPresent(Supplier valueSupplier) { - super(valueSupplier, Objects::nonNull); +public class IsLessThanOrEqualToWhenPresent extends AbstractSingleValueCondition + implements AbstractSingleValueCondition.Filterable, AbstractSingleValueCondition.Mappable { + private static final IsLessThanOrEqualToWhenPresent EMPTY = new IsLessThanOrEqualToWhenPresent(-1) { + @Override + public Object value() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } + + @Override + public boolean isEmpty() { + return true; + } + }; + + public static IsLessThanOrEqualToWhenPresent empty() { + @SuppressWarnings("unchecked") + IsLessThanOrEqualToWhenPresent t = (IsLessThanOrEqualToWhenPresent) EMPTY; + return t; + } + + protected IsLessThanOrEqualToWhenPresent(T value) { + super(value); } - - public static IsLessThanOrEqualToWhenPresent of(Supplier valueSupplier) { - return new IsLessThanOrEqualToWhenPresent<>(valueSupplier); + + @Override + public String operator() { + return "<="; //$NON-NLS-1$ + } + + public static IsLessThanOrEqualToWhenPresent of(@Nullable T value) { + if (value == null) { + return empty(); + } else { + return new IsLessThanOrEqualToWhenPresent<>(value); + } + } + + @Override + public IsLessThanOrEqualToWhenPresent filter(Predicate predicate) { + return filterSupport(predicate, IsLessThanOrEqualToWhenPresent::empty, this); } @Override - public IsLessThanOrEqualToWhenPresent then(UnaryOperator transformer) { - return shouldRender() ? new IsLessThanOrEqualToWhenPresent<>(() -> transformer.apply(value())) : this; + public IsLessThanOrEqualToWhenPresent map(Function mapper) { + return mapSupport(mapper, IsLessThanOrEqualToWhenPresent::of, IsLessThanOrEqualToWhenPresent::empty); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanOrEqualToWithSubselect.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanOrEqualToWithSubselect.java index 57f00a088..7a7769016 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanOrEqualToWithSubselect.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanOrEqualToWithSubselect.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -20,7 +20,7 @@ import org.mybatis.dynamic.sql.util.Buildable; public class IsLessThanOrEqualToWithSubselect extends AbstractSubselectCondition { - + protected IsLessThanOrEqualToWithSubselect(Buildable selectModelBuilder) { super(selectModelBuilder); } @@ -30,7 +30,7 @@ public static IsLessThanOrEqualToWithSubselect of(Buildable } @Override - public String renderCondition(String columnName, String renderedSelectStatement) { - return columnName + " <= (" + renderedSelectStatement + ")"; //$NON-NLS-1$ //$NON-NLS-2$ + public String operator() { + return "<="; //$NON-NLS-1$ } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanWhenPresent.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanWhenPresent.java index 6ae4f4988..78a07f9a2 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanWhenPresent.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanWhenPresent.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,22 +15,58 @@ */ package org.mybatis.dynamic.sql.where.condition; -import java.util.Objects; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; +import java.util.NoSuchElementException; +import java.util.function.Function; +import java.util.function.Predicate; -public class IsLessThanWhenPresent extends IsLessThan { +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.AbstractSingleValueCondition; - protected IsLessThanWhenPresent(Supplier valueSupplier) { - super(valueSupplier, Objects::nonNull); +public class IsLessThanWhenPresent extends AbstractSingleValueCondition + implements AbstractSingleValueCondition.Filterable, AbstractSingleValueCondition.Mappable { + + private static final IsLessThanWhenPresent EMPTY = new IsLessThanWhenPresent(-1) { + @Override + public Object value() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } + + @Override + public boolean isEmpty() { + return true; + } + }; + + public static IsLessThanWhenPresent empty() { + @SuppressWarnings("unchecked") + IsLessThanWhenPresent t = (IsLessThanWhenPresent) EMPTY; + return t; + } + + protected IsLessThanWhenPresent(T value) { + super(value); + } + + @Override + public String operator() { + return "<"; //$NON-NLS-1$ + } + + public static IsLessThanWhenPresent of(@Nullable T value) { + if (value == null) { + return empty(); + } else { + return new IsLessThanWhenPresent<>(value); + } } - - public static IsLessThanWhenPresent of(Supplier valueSupplier) { - return new IsLessThanWhenPresent<>(valueSupplier); + + @Override + public IsLessThanWhenPresent filter(Predicate predicate) { + return filterSupport(predicate, IsLessThanWhenPresent::empty, this); } @Override - public IsLessThanWhenPresent then(UnaryOperator transformer) { - return shouldRender() ? new IsLessThanWhenPresent<>(() -> transformer.apply(value())) : this; + public IsLessThanWhenPresent map(Function mapper) { + return mapSupport(mapper, IsLessThanWhenPresent::of, IsLessThanWhenPresent::empty); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanWithSubselect.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanWithSubselect.java index 488503577..91a2765a4 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanWithSubselect.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanWithSubselect.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -20,7 +20,7 @@ import org.mybatis.dynamic.sql.util.Buildable; public class IsLessThanWithSubselect extends AbstractSubselectCondition { - + protected IsLessThanWithSubselect(Buildable selectModelBuilder) { super(selectModelBuilder); } @@ -30,7 +30,7 @@ public static IsLessThanWithSubselect of(Buildable selectMod } @Override - public String renderCondition(String columnName, String renderedSelectStatement) { - return columnName + " < (" + renderedSelectStatement + ")"; //$NON-NLS-1$ //$NON-NLS-2$ + public String operator() { + return "<"; //$NON-NLS-1$ } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLike.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLike.java index 4d9a55bd6..e738bda4c 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLike.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLike.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,36 +15,53 @@ */ package org.mybatis.dynamic.sql.where.condition; +import java.util.NoSuchElementException; +import java.util.function.Function; import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; import org.mybatis.dynamic.sql.AbstractSingleValueCondition; -public class IsLike extends AbstractSingleValueCondition { +public class IsLike extends AbstractSingleValueCondition + implements AbstractSingleValueCondition.Filterable, AbstractSingleValueCondition.Mappable { - protected IsLike(Supplier valueSupplier) { - super(valueSupplier); + private static final IsLike EMPTY = new IsLike(-1) { + @Override + public Object value() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } + + @Override + public boolean isEmpty() { + return true; + } + }; + + public static IsLike empty() { + @SuppressWarnings("unchecked") + IsLike t = (IsLike) EMPTY; + return t; } - protected IsLike(Supplier valueSupplier, Predicate predicate) { - super(valueSupplier, predicate); + protected IsLike(T value) { + super(value); } @Override - public String renderCondition(String columnName, String placeholder) { - return columnName + " like " + placeholder; //$NON-NLS-1$ + public String operator() { + return "like"; //$NON-NLS-1$ } - - public static IsLike of(Supplier valueSupplier) { - return new IsLike<>(valueSupplier); + + public static IsLike of(T value) { + return new IsLike<>(value); } - - public IsLike when(Predicate predicate) { - return new IsLike<>(valueSupplier, predicate); + + @Override + public IsLike filter(Predicate predicate) { + return filterSupport(predicate, IsLike::empty, this); } - public IsLike then(UnaryOperator transformer) { - return shouldRender() ? new IsLike<>(() -> transformer.apply(value())) : this; + @Override + public IsLike map(Function mapper) { + return mapSupport(mapper, IsLike::new, IsLike::empty); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLikeCaseInsensitive.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLikeCaseInsensitive.java index d2b67204a..ffdc2bc7d 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLikeCaseInsensitive.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLikeCaseInsensitive.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,41 +15,54 @@ */ package org.mybatis.dynamic.sql.where.condition; +import java.util.NoSuchElementException; +import java.util.function.Function; import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; import org.mybatis.dynamic.sql.AbstractSingleValueCondition; import org.mybatis.dynamic.sql.util.StringUtilities; -public class IsLikeCaseInsensitive extends AbstractSingleValueCondition { - protected IsLikeCaseInsensitive(Supplier valueSupplier) { - super(valueSupplier); +public class IsLikeCaseInsensitive extends AbstractSingleValueCondition + implements CaseInsensitiveRenderableCondition, AbstractSingleValueCondition.Filterable, + AbstractSingleValueCondition.Mappable { + private static final IsLikeCaseInsensitive EMPTY = new IsLikeCaseInsensitive<>("") { //$NON-NLS-1$ + @Override + public String value() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } + + @Override + public boolean isEmpty() { + return true; + } + }; + + public static IsLikeCaseInsensitive empty() { + @SuppressWarnings("unchecked") + IsLikeCaseInsensitive t = (IsLikeCaseInsensitive) EMPTY; + return t; } - - protected IsLikeCaseInsensitive(Supplier valueSupplier, Predicate predicate) { - super(valueSupplier, predicate); + + protected IsLikeCaseInsensitive(T value) { + super(StringUtilities.upperCaseIfPossible(value)); } - + @Override - public String renderCondition(String columnName, String placeholder) { - return "upper(" + columnName + ") like " + placeholder; //$NON-NLS-1$ //$NON-NLS-2$ + public String operator() { + return "like"; //$NON-NLS-1$ } @Override - public String value() { - return StringUtilities.safelyUpperCase(super.value()); + public IsLikeCaseInsensitive filter(Predicate predicate) { + return filterSupport(predicate, IsLikeCaseInsensitive::empty, this); } - public static IsLikeCaseInsensitive of(Supplier valueSupplier) { - return new IsLikeCaseInsensitive(valueSupplier); - } - - public IsLikeCaseInsensitive when(Predicate predicate) { - return new IsLikeCaseInsensitive(valueSupplier, predicate); + @Override + public IsLikeCaseInsensitive map(Function mapper) { + return mapSupport(mapper, IsLikeCaseInsensitive::new, IsLikeCaseInsensitive::empty); } - public IsLikeCaseInsensitive then(UnaryOperator transformer) { - return shouldRender() ? new IsLikeCaseInsensitive(() -> transformer.apply(value())) : this; + public static IsLikeCaseInsensitive of(T value) { + return new IsLikeCaseInsensitive<>(value); } -} \ No newline at end of file +} diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLikeCaseInsensitiveWhenPresent.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLikeCaseInsensitiveWhenPresent.java index 1609efa0f..0f06e3311 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLikeCaseInsensitiveWhenPresent.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLikeCaseInsensitiveWhenPresent.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,22 +15,60 @@ */ package org.mybatis.dynamic.sql.where.condition; -import java.util.Objects; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; +import java.util.NoSuchElementException; +import java.util.function.Function; +import java.util.function.Predicate; -public class IsLikeCaseInsensitiveWhenPresent extends IsLikeCaseInsensitive { +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.AbstractSingleValueCondition; +import org.mybatis.dynamic.sql.util.StringUtilities; - protected IsLikeCaseInsensitiveWhenPresent(Supplier valueSupplier) { - super(valueSupplier, Objects::nonNull); +public class IsLikeCaseInsensitiveWhenPresent extends AbstractSingleValueCondition + implements CaseInsensitiveRenderableCondition, AbstractSingleValueCondition.Filterable, + AbstractSingleValueCondition.Mappable { + private static final IsLikeCaseInsensitiveWhenPresent EMPTY = + new IsLikeCaseInsensitiveWhenPresent<>("") { //$NON-NLS-1$ + @Override + public String value() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } + + @Override + public boolean isEmpty() { + return true; + } + }; + + public static IsLikeCaseInsensitiveWhenPresent empty() { + @SuppressWarnings("unchecked") + IsLikeCaseInsensitiveWhenPresent t = (IsLikeCaseInsensitiveWhenPresent) EMPTY; + return t; + } + + protected IsLikeCaseInsensitiveWhenPresent(T value) { + super(StringUtilities.upperCaseIfPossible(value)); } - - public static IsLikeCaseInsensitiveWhenPresent of(Supplier valueSupplier) { - return new IsLikeCaseInsensitiveWhenPresent(valueSupplier); + + @Override + public String operator() { + return "like"; //$NON-NLS-1$ } @Override - public IsLikeCaseInsensitiveWhenPresent then(UnaryOperator transformer) { - return shouldRender() ? new IsLikeCaseInsensitiveWhenPresent(() -> transformer.apply(value())) : this; + public IsLikeCaseInsensitiveWhenPresent filter(Predicate predicate) { + return filterSupport(predicate, IsLikeCaseInsensitiveWhenPresent::empty, this); + } + + @Override + public IsLikeCaseInsensitiveWhenPresent map(Function mapper) { + return mapSupport(mapper, IsLikeCaseInsensitiveWhenPresent::of, IsLikeCaseInsensitiveWhenPresent::empty); + } + + public static IsLikeCaseInsensitiveWhenPresent of(@Nullable T value) { + if (value == null) { + return empty(); + } else { + return new IsLikeCaseInsensitiveWhenPresent<>(value); + } } -} \ No newline at end of file +} diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLikeWhenPresent.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLikeWhenPresent.java index e4beea97a..a69e55356 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLikeWhenPresent.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLikeWhenPresent.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,22 +15,58 @@ */ package org.mybatis.dynamic.sql.where.condition; -import java.util.Objects; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; +import java.util.NoSuchElementException; +import java.util.function.Function; +import java.util.function.Predicate; -public class IsLikeWhenPresent extends IsLike { +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.AbstractSingleValueCondition; - protected IsLikeWhenPresent(Supplier valueSupplier) { - super(valueSupplier, Objects::nonNull); +public class IsLikeWhenPresent extends AbstractSingleValueCondition + implements AbstractSingleValueCondition.Filterable, AbstractSingleValueCondition.Mappable { + + private static final IsLikeWhenPresent EMPTY = new IsLikeWhenPresent(-1) { + @Override + public Object value() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } + + @Override + public boolean isEmpty() { + return true; + } + }; + + public static IsLikeWhenPresent empty() { + @SuppressWarnings("unchecked") + IsLikeWhenPresent t = (IsLikeWhenPresent) EMPTY; + return t; } - public static IsLikeWhenPresent of(Supplier valueSupplier) { - return new IsLikeWhenPresent<>(valueSupplier); + protected IsLikeWhenPresent(T value) { + super(value); + } + + @Override + public String operator() { + return "like"; //$NON-NLS-1$ + } + + public static IsLikeWhenPresent of(@Nullable T value) { + if (value == null) { + return empty(); + } else { + return new IsLikeWhenPresent<>(value); + } + } + + @Override + public IsLikeWhenPresent filter(Predicate predicate) { + return filterSupport(predicate, IsLikeWhenPresent::empty, this); } @Override - public IsLikeWhenPresent then(UnaryOperator transformer) { - return shouldRender() ? new IsLikeWhenPresent<>(() -> transformer.apply(value())) : this; + public IsLikeWhenPresent map(Function mapper) { + return mapSupport(mapper, IsLikeWhenPresent::of, IsLikeWhenPresent::empty); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotBetween.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotBetween.java index 0a3b1b2f0..836e3c741 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotBetween.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotBetween.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,49 +15,86 @@ */ package org.mybatis.dynamic.sql.where.condition; +import java.util.NoSuchElementException; import java.util.function.BiPredicate; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; +import java.util.function.Function; +import java.util.function.Predicate; import org.mybatis.dynamic.sql.AbstractTwoValueCondition; -public class IsNotBetween extends AbstractTwoValueCondition { +public class IsNotBetween extends AbstractTwoValueCondition + implements AbstractTwoValueCondition.Filterable, AbstractTwoValueCondition.Mappable { + private static final IsNotBetween EMPTY = new IsNotBetween(-1, -1) { + @Override + public Object value1() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } + + @Override + public Object value2() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } + + @Override + public boolean isEmpty() { + return true; + } + }; - protected IsNotBetween(Supplier valueSupplier1, Supplier valueSupplier2) { - super(valueSupplier1, valueSupplier2); + public static IsNotBetween empty() { + @SuppressWarnings("unchecked") + IsNotBetween t = (IsNotBetween) EMPTY; + return t; } - - protected IsNotBetween(Supplier valueSupplier1, Supplier valueSupplier2, BiPredicate predicate) { - super(valueSupplier1, valueSupplier2, predicate); + + protected IsNotBetween(T value1, T value2) { + super(value1, value2); } - + @Override - public String renderCondition(String columnName, String placeholder1, String placeholder2) { - return columnName + " not between " + placeholder1 + " and " + placeholder2; //$NON-NLS-1$ //$NON-NLS-2$ + public String operator1() { + return "not between"; //$NON-NLS-1$ } - public static class Builder extends AndGatherer> { - - private Builder(Supplier valueSupplier1) { - super(valueSupplier1); - } + @Override + public String operator2() { + return "and"; //$NON-NLS-1$ + } - @Override - protected IsNotBetween build() { - return new IsNotBetween<>(valueSupplier1, valueSupplier2); - } + @Override + public IsNotBetween filter(BiPredicate predicate) { + return filterSupport(predicate, IsNotBetween::empty, this); + } + + @Override + public IsNotBetween filter(Predicate predicate) { + return filterSupport(predicate, IsNotBetween::empty, this); } - - public static Builder isNotBetween(Supplier valueSupplier1) { - return new Builder<>(valueSupplier1); + + @Override + public IsNotBetween map(Function mapper1, + Function mapper2) { + return mapSupport(mapper1, mapper2, IsNotBetween::new, IsNotBetween::empty); + } + + @Override + public IsNotBetween map(Function mapper) { + return map(mapper, mapper); } - - public IsNotBetween when(BiPredicate predicate) { - return new IsNotBetween<>(valueSupplier1, valueSupplier2, predicate); + + public static Builder isNotBetween(T value1) { + return new Builder<>(value1); } - public IsNotBetween then(UnaryOperator transformer1, UnaryOperator transformer2) { - return shouldRender() ? new IsNotBetween<>(() -> transformer1.apply(value1()), - () -> transformer2.apply(value2())) : this; + public static class Builder extends AndGatherer> { + + private Builder(T value1) { + super(value1); + } + + @Override + protected IsNotBetween build(T value2) { + return new IsNotBetween<>(value1, value2); + } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotBetweenWhenPresent.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotBetweenWhenPresent.java index fd0ba0f48..020b651f2 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotBetweenWhenPresent.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotBetweenWhenPresent.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,35 +15,95 @@ */ package org.mybatis.dynamic.sql.where.condition; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; +import java.util.NoSuchElementException; +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.function.Predicate; -import org.mybatis.dynamic.sql.util.Predicates; +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.AbstractTwoValueCondition; -public class IsNotBetweenWhenPresent extends IsNotBetween { +public class IsNotBetweenWhenPresent extends AbstractTwoValueCondition + implements AbstractTwoValueCondition.Filterable, AbstractTwoValueCondition.Mappable { + private static final IsNotBetweenWhenPresent EMPTY = new IsNotBetweenWhenPresent(-1, -1) { + @Override + public Object value1() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } - protected IsNotBetweenWhenPresent(Supplier valueSupplier1, Supplier valueSupplier2) { - super(valueSupplier1, valueSupplier2, Predicates.bothPresent()); - } - - public static class Builder extends AndGatherer> { - private Builder(Supplier valueSupplier1) { - super(valueSupplier1); + @Override + public Object value2() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ } - + @Override - protected IsNotBetweenWhenPresent build() { - return new IsNotBetweenWhenPresent<>(valueSupplier1, valueSupplier2); + public boolean isEmpty() { + return true; } + }; + + public static IsNotBetweenWhenPresent empty() { + @SuppressWarnings("unchecked") + IsNotBetweenWhenPresent t = (IsNotBetweenWhenPresent) EMPTY; + return t; + } + + protected IsNotBetweenWhenPresent(T value1, T value2) { + super(value1, value2); + } + + @Override + public String operator1() { + return "not between"; //$NON-NLS-1$ + } + + @Override + public String operator2() { + return "and"; //$NON-NLS-1$ + } + + @Override + public IsNotBetweenWhenPresent filter(BiPredicate predicate) { + return filterSupport(predicate, IsNotBetweenWhenPresent::empty, this); } - - public static Builder isNotBetweenWhenPresent(Supplier valueSupplier) { - return new Builder<>(valueSupplier); + + @Override + public IsNotBetweenWhenPresent filter(Predicate predicate) { + return filterSupport(predicate, IsNotBetweenWhenPresent::empty, this); + } + + @Override + public IsNotBetweenWhenPresent map(Function mapper1, + Function mapper2) { + return mapSupport(mapper1, mapper2, IsNotBetweenWhenPresent::of, IsNotBetweenWhenPresent::empty); } @Override - public IsNotBetweenWhenPresent then(UnaryOperator transformer1, UnaryOperator transformer2) { - return shouldRender() ? new IsNotBetweenWhenPresent<>(() -> transformer1.apply(value1()), - () -> transformer2.apply(value2())) : this; + public IsNotBetweenWhenPresent map(Function mapper) { + return map(mapper, mapper); + } + + public static IsNotBetweenWhenPresent of(@Nullable T value1, @Nullable T value2) { + if (value1 == null || value2 == null) { + return empty(); + } else { + return new IsNotBetweenWhenPresent<>(value1, value2); + } + } + + public static Builder isNotBetweenWhenPresent(@Nullable T value1) { + return new Builder<>(value1); + } + + public static class Builder extends AndWhenPresentGatherer> { + + private Builder(@Nullable T value1) { + super(value1); + } + + @Override + protected IsNotBetweenWhenPresent build(@Nullable T value2) { + return of(value1, value2); + } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotEqualTo.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotEqualTo.java index 77d06c9e8..39070c2e8 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotEqualTo.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotEqualTo.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,36 +15,52 @@ */ package org.mybatis.dynamic.sql.where.condition; +import java.util.NoSuchElementException; +import java.util.function.Function; import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; import org.mybatis.dynamic.sql.AbstractSingleValueCondition; -public class IsNotEqualTo extends AbstractSingleValueCondition { +public class IsNotEqualTo extends AbstractSingleValueCondition + implements AbstractSingleValueCondition.Filterable, AbstractSingleValueCondition.Mappable { + private static final IsNotEqualTo EMPTY = new IsNotEqualTo(-1) { + @Override + public Object value() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } - protected IsNotEqualTo(Supplier valueSupplier) { - super(valueSupplier); + @Override + public boolean isEmpty() { + return true; + } + }; + + public static IsNotEqualTo empty() { + @SuppressWarnings("unchecked") + IsNotEqualTo t = (IsNotEqualTo) EMPTY; + return t; } - protected IsNotEqualTo(Supplier valueSupplier, Predicate predicate) { - super(valueSupplier, predicate); + protected IsNotEqualTo(T value) { + super(value); } @Override - public String renderCondition(String columnName, String placeholder) { - return columnName + " <> " + placeholder; //$NON-NLS-1$ + public String operator() { + return "<>"; //$NON-NLS-1$ } - - public static IsNotEqualTo of(Supplier valueSupplier) { - return new IsNotEqualTo<>(valueSupplier); + + public static IsNotEqualTo of(T value) { + return new IsNotEqualTo<>(value); } - - public IsNotEqualTo when(Predicate predicate) { - return new IsNotEqualTo<>(valueSupplier, predicate); + + @Override + public IsNotEqualTo filter(Predicate predicate) { + return filterSupport(predicate, IsNotEqualTo::empty, this); } - public IsNotEqualTo then(UnaryOperator transformer) { - return shouldRender() ? new IsNotEqualTo<>(() -> transformer.apply(value())) : this; + @Override + public IsNotEqualTo map(Function mapper) { + return mapSupport(mapper, IsNotEqualTo::new, IsNotEqualTo::empty); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotEqualToColumn.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotEqualToColumn.java index c1e365ca8..c8721b035 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotEqualToColumn.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotEqualToColumn.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -25,10 +25,10 @@ protected IsNotEqualToColumn(BasicColumn column) { } @Override - protected String renderCondition(String leftColumn, String rightColumn) { - return leftColumn + " <> " + rightColumn; //$NON-NLS-1$ + public String operator() { + return "<>"; //$NON-NLS-1$ } - + public static IsNotEqualToColumn of(BasicColumn column) { return new IsNotEqualToColumn<>(column); } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotEqualToWhenPresent.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotEqualToWhenPresent.java index 86bca9be4..07ab3f6cf 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotEqualToWhenPresent.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotEqualToWhenPresent.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,22 +15,57 @@ */ package org.mybatis.dynamic.sql.where.condition; -import java.util.Objects; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; +import java.util.NoSuchElementException; +import java.util.function.Function; +import java.util.function.Predicate; -public class IsNotEqualToWhenPresent extends IsNotEqualTo { +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.AbstractSingleValueCondition; - protected IsNotEqualToWhenPresent(Supplier valueSupplier) { - super(valueSupplier, Objects::nonNull); +public class IsNotEqualToWhenPresent extends AbstractSingleValueCondition + implements AbstractSingleValueCondition.Filterable, AbstractSingleValueCondition.Mappable { + private static final IsNotEqualToWhenPresent EMPTY = new IsNotEqualToWhenPresent(-1) { + @Override + public Object value() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } + + @Override + public boolean isEmpty() { + return true; + } + }; + + public static IsNotEqualToWhenPresent empty() { + @SuppressWarnings("unchecked") + IsNotEqualToWhenPresent t = (IsNotEqualToWhenPresent) EMPTY; + return t; + } + + protected IsNotEqualToWhenPresent(T value) { + super(value); + } + + @Override + public String operator() { + return "<>"; //$NON-NLS-1$ } - public static IsNotEqualToWhenPresent of(Supplier valueSupplier) { - return new IsNotEqualToWhenPresent<>(valueSupplier); + public static IsNotEqualToWhenPresent of(@Nullable T value) { + if (value == null) { + return empty(); + } else { + return new IsNotEqualToWhenPresent<>(value); + } + } + + @Override + public IsNotEqualToWhenPresent filter(Predicate predicate) { + return filterSupport(predicate, IsNotEqualToWhenPresent::empty, this); } @Override - public IsNotEqualToWhenPresent then(UnaryOperator transformer) { - return shouldRender() ? new IsNotEqualToWhenPresent<>(() -> transformer.apply(value())) : this; + public IsNotEqualToWhenPresent map(Function mapper) { + return mapSupport(mapper, IsNotEqualToWhenPresent::of, IsNotEqualToWhenPresent::empty); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotEqualToWithSubselect.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotEqualToWithSubselect.java index cf48462e2..2e19d9a19 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotEqualToWithSubselect.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotEqualToWithSubselect.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -20,7 +20,7 @@ import org.mybatis.dynamic.sql.util.Buildable; public class IsNotEqualToWithSubselect extends AbstractSubselectCondition { - + protected IsNotEqualToWithSubselect(Buildable selectModelBuilder) { super(selectModelBuilder); } @@ -30,7 +30,7 @@ public static IsNotEqualToWithSubselect of(Buildable selectM } @Override - public String renderCondition(String columnName, String renderedSelectStatement) { - return columnName + " <> (" + renderedSelectStatement + ")"; //$NON-NLS-1$ //$NON-NLS-2$ + public String operator() { + return "<>"; //$NON-NLS-1$ } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotIn.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotIn.java index ec617468e..e6b408fc8 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotIn.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotIn.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,19 +15,24 @@ */ package org.mybatis.dynamic.sql.where.condition; -import static org.mybatis.dynamic.sql.util.StringUtilities.spaceAfter; - +import java.util.Arrays; import java.util.Collection; -import java.util.function.UnaryOperator; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import java.util.Collections; +import java.util.function.Function; +import java.util.function.Predicate; import org.mybatis.dynamic.sql.AbstractListValueCondition; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.Validator; -public class IsNotIn extends AbstractListValueCondition { +public class IsNotIn extends AbstractListValueCondition + implements AbstractListValueCondition.Filterable, AbstractListValueCondition.Mappable { + private static final IsNotIn EMPTY = new IsNotIn<>(Collections.emptyList()); - protected IsNotIn(Collection values, UnaryOperator> valueStreamTransformer) { - super(values, valueStreamTransformer); + public static IsNotIn empty() { + @SuppressWarnings("unchecked") + IsNotIn t = (IsNotIn) EMPTY; + return t; } protected IsNotIn(Collection values) { @@ -35,24 +40,29 @@ protected IsNotIn(Collection values) { } @Override - public String renderCondition(String columnName, Stream placeholders) { - return spaceAfter(columnName) - + placeholders.collect( - Collectors.joining(",", "not in (", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - } - - /** - * This method allows you to modify the condition's values before they are placed into the parameter map. - * For example, you could filter nulls, or trim strings, etc. This process will run before final rendering of SQL. - * If you filter values out of the stream, then final condition will not reference those values. If you filter all - * values out of the stream, then the condition will not render. - * - * @param valueStreamTransformer a UnaryOperator that will transform the value stream before - * the values are placed in the parameter map - * @return new condition with the specified transformer - */ - public IsNotIn then(UnaryOperator> valueStreamTransformer) { - return new IsNotIn<>(values, valueStreamTransformer); + public boolean shouldRender(RenderingContext renderingContext) { + Validator.assertNotEmpty(values, "ERROR.44", "IsNotIn"); //$NON-NLS-1$ //$NON-NLS-2$ + return true; + } + + @Override + public String operator() { + return "not in"; //$NON-NLS-1$ + } + + @Override + public IsNotIn filter(Predicate predicate) { + return filterSupport(predicate, IsNotIn::new, this, IsNotIn::empty); + } + + @Override + public IsNotIn map(Function mapper) { + return mapSupport(mapper, IsNotIn::new, IsNotIn::empty); + } + + @SafeVarargs + public static IsNotIn of(T... values) { + return of(Arrays.asList(values)); } public static IsNotIn of(Collection values) { diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInCaseInsensitive.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInCaseInsensitive.java index 6c6a40269..2bc802ab8 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInCaseInsensitive.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInCaseInsensitive.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,46 +15,59 @@ */ package org.mybatis.dynamic.sql.where.condition; +import java.util.Arrays; import java.util.Collection; -import java.util.function.UnaryOperator; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import java.util.Collections; +import java.util.function.Function; +import java.util.function.Predicate; import org.mybatis.dynamic.sql.AbstractListValueCondition; +import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.util.StringUtilities; +import org.mybatis.dynamic.sql.util.Validator; -public class IsNotInCaseInsensitive extends AbstractListValueCondition { +public class IsNotInCaseInsensitive extends AbstractListValueCondition + implements CaseInsensitiveRenderableCondition, AbstractListValueCondition.Filterable, + AbstractListValueCondition.Mappable { + private static final IsNotInCaseInsensitive EMPTY = new IsNotInCaseInsensitive<>(Collections.emptyList()); - protected IsNotInCaseInsensitive(Collection values) { - super(values, s -> s.map(StringUtilities::safelyUpperCase)); + public static IsNotInCaseInsensitive empty() { + @SuppressWarnings("unchecked") + IsNotInCaseInsensitive t = (IsNotInCaseInsensitive) EMPTY; + return t; } - protected IsNotInCaseInsensitive(Collection values, UnaryOperator> valueStreamTransformer) { - super(values, StringUtilities.upperCaseAfter(valueStreamTransformer)); + protected IsNotInCaseInsensitive(Collection values) { + super(values.stream().map(StringUtilities::upperCaseIfPossible).toList()); } @Override - public String renderCondition(String columnName, Stream placeholders) { - return "upper(" + columnName + ") " + //$NON-NLS-1$ //$NON-NLS-2$ - placeholders.collect( - Collectors.joining(",", "not in (", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - } - - /** - * This method allows you to modify the condition's values before they are placed into the parameter map. - * For example, you could filter nulls, or trim strings, etc. This process will run before final rendering of SQL. - * If you filter values out of the stream, then final condition will not reference those values. If you filter all - * values out of the stream, then the condition will not render. - * - * @param valueStreamTransformer a UnaryOperator that will transform the value stream before - * the values are placed in the parameter map - * @return new condition with the specified transformer - */ - public IsNotInCaseInsensitive then(UnaryOperator> valueStreamTransformer) { - return new IsNotInCaseInsensitive(values, valueStreamTransformer); - } - - public static IsNotInCaseInsensitive of(Collection values) { - return new IsNotInCaseInsensitive(values); + public boolean shouldRender(RenderingContext renderingContext) { + Validator.assertNotEmpty(values, "ERROR.44", "IsNotInCaseInsensitive"); //$NON-NLS-1$ //$NON-NLS-2$ + return true; + } + + @Override + public String operator() { + return "not in"; //$NON-NLS-1$ + } + + @Override + public IsNotInCaseInsensitive filter(Predicate predicate) { + return filterSupport(predicate, IsNotInCaseInsensitive::new, this, IsNotInCaseInsensitive::empty); + } + + @Override + public IsNotInCaseInsensitive map(Function mapper) { + return mapSupport(mapper, IsNotInCaseInsensitive::new, IsNotInCaseInsensitive::empty); + } + + @SafeVarargs + public static IsNotInCaseInsensitive of(T... values) { + return of(Arrays.asList(values)); + } + + public static IsNotInCaseInsensitive of(Collection values) { + return new IsNotInCaseInsensitive<>(values); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInCaseInsensitiveWhenPresent.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInCaseInsensitiveWhenPresent.java index 85fdf3f5f..2640e062f 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInCaseInsensitiveWhenPresent.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInCaseInsensitiveWhenPresent.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,16 +15,59 @@ */ package org.mybatis.dynamic.sql.where.condition; +import java.util.Arrays; import java.util.Collection; -import java.util.Objects; +import java.util.Collections; +import java.util.function.Function; +import java.util.function.Predicate; -public class IsNotInCaseInsensitiveWhenPresent extends IsNotInCaseInsensitive { +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.AbstractListValueCondition; +import org.mybatis.dynamic.sql.util.StringUtilities; +import org.mybatis.dynamic.sql.util.Utilities; - protected IsNotInCaseInsensitiveWhenPresent(Collection values) { - super(values, s -> s.filter(Objects::nonNull)); +public class IsNotInCaseInsensitiveWhenPresent extends AbstractListValueCondition + implements CaseInsensitiveRenderableCondition, AbstractListValueCondition.Filterable, + AbstractListValueCondition.Mappable { + private static final IsNotInCaseInsensitiveWhenPresent EMPTY = + new IsNotInCaseInsensitiveWhenPresent<>(Collections.emptyList()); + + public static IsNotInCaseInsensitiveWhenPresent empty() { + @SuppressWarnings("unchecked") + IsNotInCaseInsensitiveWhenPresent t = (IsNotInCaseInsensitiveWhenPresent) EMPTY; + return t; + } + + protected IsNotInCaseInsensitiveWhenPresent(Collection values) { + super(Utilities.filterNulls(values).map(StringUtilities::upperCaseIfPossible).toList()); + } + + @Override + public String operator() { + return "not in"; //$NON-NLS-1$ + } + + @Override + public IsNotInCaseInsensitiveWhenPresent filter(Predicate predicate) { + return filterSupport(predicate, IsNotInCaseInsensitiveWhenPresent::new, + this, IsNotInCaseInsensitiveWhenPresent::empty); + } + + @Override + public IsNotInCaseInsensitiveWhenPresent map(Function mapper) { + return mapSupport(mapper, IsNotInCaseInsensitiveWhenPresent::new, IsNotInCaseInsensitiveWhenPresent::empty); + } + + @SafeVarargs + public static IsNotInCaseInsensitiveWhenPresent of(@Nullable T... values) { + return of(Arrays.asList(values)); } - public static IsNotInCaseInsensitiveWhenPresent of(Collection values) { - return new IsNotInCaseInsensitiveWhenPresent(values); + public static IsNotInCaseInsensitiveWhenPresent of(@Nullable Collection<@Nullable T> values) { + if (values == null) { + return empty(); + } else { + return new IsNotInCaseInsensitiveWhenPresent<>(values); + } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInWhenPresent.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInWhenPresent.java index 55bf9fd70..6624a50ad 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInWhenPresent.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInWhenPresent.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,16 +15,55 @@ */ package org.mybatis.dynamic.sql.where.condition; +import java.util.Arrays; import java.util.Collection; -import java.util.Objects; +import java.util.Collections; +import java.util.function.Function; +import java.util.function.Predicate; -public class IsNotInWhenPresent extends IsNotIn { +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.AbstractListValueCondition; +import org.mybatis.dynamic.sql.util.Utilities; - protected IsNotInWhenPresent(Collection values) { - super(values, s -> s.filter(Objects::nonNull)); +public class IsNotInWhenPresent extends AbstractListValueCondition + implements AbstractListValueCondition.Filterable, AbstractListValueCondition.Mappable { + private static final IsNotInWhenPresent EMPTY = new IsNotInWhenPresent<>(Collections.emptyList()); + + public static IsNotInWhenPresent empty() { + @SuppressWarnings("unchecked") + IsNotInWhenPresent t = (IsNotInWhenPresent) EMPTY; + return t; + } + + protected IsNotInWhenPresent(Collection<@Nullable T> values) { + super(Utilities.filterNulls(values).toList()); + } + + @Override + public String operator() { + return "not in"; //$NON-NLS-1$ + } + + @Override + public IsNotInWhenPresent filter(Predicate predicate) { + return filterSupport(predicate, IsNotInWhenPresent::new, this, IsNotInWhenPresent::empty); + } + + @Override + public IsNotInWhenPresent map(Function mapper) { + return mapSupport(mapper, IsNotInWhenPresent::new, IsNotInWhenPresent::empty); + } + + @SafeVarargs + public static IsNotInWhenPresent of(@Nullable T... values) { + return of(Arrays.asList(values)); } - public static IsNotInWhenPresent of(Collection values) { - return new IsNotInWhenPresent<>(values); + public static IsNotInWhenPresent of(@Nullable Collection<@Nullable T> values) { + if (values == null) { + return empty(); + } else { + return new IsNotInWhenPresent<>(values); + } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInWithSubselect.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInWithSubselect.java index 083189922..f6f3764f1 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInWithSubselect.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInWithSubselect.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -20,7 +20,7 @@ import org.mybatis.dynamic.sql.util.Buildable; public class IsNotInWithSubselect extends AbstractSubselectCondition { - + protected IsNotInWithSubselect(Buildable selectModelBuilder) { super(selectModelBuilder); } @@ -30,7 +30,7 @@ public static IsNotInWithSubselect of(Buildable selectModelB } @Override - public String renderCondition(String columnName, String renderedSelectStatement) { - return columnName + " not in (" + renderedSelectStatement + ")"; //$NON-NLS-1$ //$NON-NLS-2$ + public String operator() { + return "not in"; //$NON-NLS-1$ } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotLike.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotLike.java index 586b26f4f..a62dc3e9e 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotLike.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotLike.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,36 +15,52 @@ */ package org.mybatis.dynamic.sql.where.condition; +import java.util.NoSuchElementException; +import java.util.function.Function; import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; import org.mybatis.dynamic.sql.AbstractSingleValueCondition; -public class IsNotLike extends AbstractSingleValueCondition { +public class IsNotLike extends AbstractSingleValueCondition + implements AbstractSingleValueCondition.Filterable, AbstractSingleValueCondition.Mappable { + private static final IsNotLike EMPTY = new IsNotLike(-1) { + @Override + public Object value() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } - protected IsNotLike(Supplier valueSupplier) { - super(valueSupplier); + @Override + public boolean isEmpty() { + return true; + } + }; + + public static IsNotLike empty() { + @SuppressWarnings("unchecked") + IsNotLike t = (IsNotLike) EMPTY; + return t; } - protected IsNotLike(Supplier valueSupplier, Predicate predicate) { - super(valueSupplier, predicate); + protected IsNotLike(T value) { + super(value); } @Override - public String renderCondition(String columnName, String placeholder) { - return columnName + " not like " + placeholder; //$NON-NLS-1$ + public String operator() { + return "not like"; //$NON-NLS-1$ } - - public static IsNotLike of(Supplier valueSupplier) { - return new IsNotLike<>(valueSupplier); + + public static IsNotLike of(T value) { + return new IsNotLike<>(value); } - - public IsNotLike when(Predicate predicate) { - return new IsNotLike<>(valueSupplier, predicate); + + @Override + public IsNotLike filter(Predicate predicate) { + return filterSupport(predicate, IsNotLike::empty, this); } - public IsNotLike then(UnaryOperator transformer) { - return shouldRender() ? new IsNotLike<>(() -> transformer.apply(value())) : this; + @Override + public IsNotLike map(Function mapper) { + return mapSupport(mapper, IsNotLike::new, IsNotLike::empty); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotLikeCaseInsensitive.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotLikeCaseInsensitive.java index 71e0dc6b4..6bd943227 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotLikeCaseInsensitive.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotLikeCaseInsensitive.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,41 +15,54 @@ */ package org.mybatis.dynamic.sql.where.condition; +import java.util.NoSuchElementException; +import java.util.function.Function; import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; import org.mybatis.dynamic.sql.AbstractSingleValueCondition; import org.mybatis.dynamic.sql.util.StringUtilities; -public class IsNotLikeCaseInsensitive extends AbstractSingleValueCondition { - protected IsNotLikeCaseInsensitive(Supplier valueSupplier) { - super(valueSupplier); +public class IsNotLikeCaseInsensitive extends AbstractSingleValueCondition + implements CaseInsensitiveRenderableCondition, AbstractSingleValueCondition.Filterable, + AbstractSingleValueCondition.Mappable { + private static final IsNotLikeCaseInsensitive EMPTY = new IsNotLikeCaseInsensitive<>("") { //$NON-NLS-1$ + @Override + public String value() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } + + @Override + public boolean isEmpty() { + return true; + } + }; + + public static IsNotLikeCaseInsensitive empty() { + @SuppressWarnings("unchecked") + IsNotLikeCaseInsensitive t = (IsNotLikeCaseInsensitive) EMPTY; + return t; } - - protected IsNotLikeCaseInsensitive(Supplier valueSupplier, Predicate predicate) { - super(valueSupplier, predicate); + + protected IsNotLikeCaseInsensitive(T value) { + super(StringUtilities.upperCaseIfPossible(value)); } - + @Override - public String renderCondition(String columnName, String placeholder) { - return "upper(" + columnName + ") not like " + placeholder; //$NON-NLS-1$ //$NON-NLS-2$ + public String operator() { + return "not like"; //$NON-NLS-1$ } - + @Override - public String value() { - return StringUtilities.safelyUpperCase(super.value()); + public IsNotLikeCaseInsensitive filter(Predicate predicate) { + return filterSupport(predicate, IsNotLikeCaseInsensitive::empty, this); } - public static IsNotLikeCaseInsensitive of(Supplier valueSupplier) { - return new IsNotLikeCaseInsensitive(valueSupplier); - } - - public IsNotLikeCaseInsensitive when(Predicate predicate) { - return new IsNotLikeCaseInsensitive(valueSupplier, predicate); + @Override + public IsNotLikeCaseInsensitive map(Function mapper) { + return mapSupport(mapper, IsNotLikeCaseInsensitive::new, IsNotLikeCaseInsensitive::empty); } - public IsNotLikeCaseInsensitive then(UnaryOperator transformer) { - return shouldRender() ? new IsNotLikeCaseInsensitive(() -> transformer.apply(value())) : this; + public static IsNotLikeCaseInsensitive of(T value) { + return new IsNotLikeCaseInsensitive<>(value); } -} \ No newline at end of file +} diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotLikeCaseInsensitiveWhenPresent.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotLikeCaseInsensitiveWhenPresent.java index 07f086908..880b20ab8 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotLikeCaseInsensitiveWhenPresent.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotLikeCaseInsensitiveWhenPresent.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,22 +15,60 @@ */ package org.mybatis.dynamic.sql.where.condition; -import java.util.Objects; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; +import java.util.NoSuchElementException; +import java.util.function.Function; +import java.util.function.Predicate; -public class IsNotLikeCaseInsensitiveWhenPresent extends IsNotLikeCaseInsensitive { +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.AbstractSingleValueCondition; +import org.mybatis.dynamic.sql.util.StringUtilities; - protected IsNotLikeCaseInsensitiveWhenPresent(Supplier valueSupplier) { - super(valueSupplier, Objects::nonNull); +public class IsNotLikeCaseInsensitiveWhenPresent extends AbstractSingleValueCondition + implements CaseInsensitiveRenderableCondition, AbstractSingleValueCondition.Filterable, + AbstractSingleValueCondition.Mappable { + private static final IsNotLikeCaseInsensitiveWhenPresent EMPTY = + new IsNotLikeCaseInsensitiveWhenPresent<>("") { //$NON-NLS-1$ + @Override + public String value() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } + + @Override + public boolean isEmpty() { + return true; + } + }; + + public static IsNotLikeCaseInsensitiveWhenPresent empty() { + @SuppressWarnings("unchecked") + IsNotLikeCaseInsensitiveWhenPresent t = (IsNotLikeCaseInsensitiveWhenPresent) EMPTY; + return t; + } + + protected IsNotLikeCaseInsensitiveWhenPresent(T value) { + super(StringUtilities.upperCaseIfPossible(value)); } - - public static IsNotLikeCaseInsensitiveWhenPresent of(Supplier valueSupplier) { - return new IsNotLikeCaseInsensitiveWhenPresent(valueSupplier); + + @Override + public String operator() { + return "not like"; //$NON-NLS-1$ } @Override - public IsNotLikeCaseInsensitiveWhenPresent then(UnaryOperator transformer) { - return shouldRender() ? new IsNotLikeCaseInsensitiveWhenPresent(() -> transformer.apply(value())) : this; + public IsNotLikeCaseInsensitiveWhenPresent filter(Predicate predicate) { + return filterSupport(predicate, IsNotLikeCaseInsensitiveWhenPresent::empty, this); + } + + @Override + public IsNotLikeCaseInsensitiveWhenPresent map(Function mapper) { + return mapSupport(mapper, IsNotLikeCaseInsensitiveWhenPresent::of, IsNotLikeCaseInsensitiveWhenPresent::empty); + } + + public static IsNotLikeCaseInsensitiveWhenPresent of(@Nullable T value) { + if (value == null) { + return empty(); + } else { + return new IsNotLikeCaseInsensitiveWhenPresent<>(value); + } } -} \ No newline at end of file +} diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotLikeWhenPresent.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotLikeWhenPresent.java index da169dc8f..d018c9062 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotLikeWhenPresent.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotLikeWhenPresent.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,22 +15,57 @@ */ package org.mybatis.dynamic.sql.where.condition; -import java.util.Objects; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; +import java.util.NoSuchElementException; +import java.util.function.Function; +import java.util.function.Predicate; -public class IsNotLikeWhenPresent extends IsNotLike { +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.AbstractSingleValueCondition; - protected IsNotLikeWhenPresent(Supplier valueSupplier) { - super(valueSupplier, Objects::nonNull); +public class IsNotLikeWhenPresent extends AbstractSingleValueCondition + implements AbstractSingleValueCondition.Filterable, AbstractSingleValueCondition.Mappable { + private static final IsNotLikeWhenPresent EMPTY = new IsNotLikeWhenPresent(-1) { + @Override + public Object value() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } + + @Override + public boolean isEmpty() { + return true; + } + }; + + public static IsNotLikeWhenPresent empty() { + @SuppressWarnings("unchecked") + IsNotLikeWhenPresent t = (IsNotLikeWhenPresent) EMPTY; + return t; + } + + protected IsNotLikeWhenPresent(T value) { + super(value); + } + + @Override + public String operator() { + return "not like"; //$NON-NLS-1$ } - public static IsNotLikeWhenPresent of(Supplier valueSupplier) { - return new IsNotLikeWhenPresent<>(valueSupplier); + public static IsNotLikeWhenPresent of(@Nullable T value) { + if (value == null) { + return empty(); + } else { + return new IsNotLikeWhenPresent<>(value); + } + } + + @Override + public IsNotLikeWhenPresent filter(Predicate predicate) { + return filterSupport(predicate, IsNotLikeWhenPresent::empty, this); } @Override - public IsNotLikeWhenPresent then(UnaryOperator transformer) { - return shouldRender() ? new IsNotLikeWhenPresent<>(() -> transformer.apply(value())) : this; + public IsNotLikeWhenPresent map(Function mapper) { + return mapSupport(mapper, IsNotLikeWhenPresent::of, IsNotLikeWhenPresent::empty); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotNull.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotNull.java index b9eda1e98..1c1f3139d 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotNull.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotNull.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -19,22 +19,33 @@ import org.mybatis.dynamic.sql.AbstractNoValueCondition; -public class IsNotNull extends AbstractNoValueCondition { +public class IsNotNull extends AbstractNoValueCondition implements AbstractNoValueCondition.Filterable { + private static final IsNotNull EMPTY = new IsNotNull<>() { + @Override + public boolean isEmpty() { + return true; + } + }; + + public static IsNotNull empty() { + @SuppressWarnings("unchecked") + IsNotNull t = (IsNotNull) EMPTY; + return t; + } public IsNotNull() { super(); } - - protected IsNotNull(BooleanSupplier booleanSupplier) { - super(booleanSupplier); - } - + @Override - public String renderCondition(String columnName) { - return columnName + " is not null"; //$NON-NLS-1$ + public String operator() { + return "is not null"; //$NON-NLS-1$ } - - public IsNotNull when(BooleanSupplier booleanSupplier) { - return new IsNotNull<>(booleanSupplier); + + @Override + public IsNotNull filter(BooleanSupplier booleanSupplier) { + @SuppressWarnings("unchecked") + IsNotNull self = (IsNotNull) this; + return filterSupport(booleanSupplier, IsNotNull::empty, self); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNull.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNull.java index 7650a6136..a27b7dc2a 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNull.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNull.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -19,22 +19,33 @@ import org.mybatis.dynamic.sql.AbstractNoValueCondition; -public class IsNull extends AbstractNoValueCondition { +public class IsNull extends AbstractNoValueCondition implements AbstractNoValueCondition.Filterable { + private static final IsNull EMPTY = new IsNull<>() { + @Override + public boolean isEmpty() { + return true; + } + }; + + public static IsNull empty() { + @SuppressWarnings("unchecked") + IsNull t = (IsNull) EMPTY; + return t; + } public IsNull() { super(); } - - protected IsNull(BooleanSupplier booleanSupplier) { - super(booleanSupplier); - } - + @Override - public String renderCondition(String columnName) { - return columnName + " is null"; //$NON-NLS-1$ + public String operator() { + return "is null"; //$NON-NLS-1$ } - - public IsNull when(BooleanSupplier booleanSupplier) { - return new IsNull<>(booleanSupplier); + + @Override + public IsNull filter(BooleanSupplier booleanSupplier) { + @SuppressWarnings("unchecked") + IsNull self = (IsNull) this; + return filterSupport(booleanSupplier, IsNull::empty, self); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/package-info.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/package-info.java new file mode 100644 index 000000000..3457063de --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +@NullMarked +package org.mybatis.dynamic.sql.where.condition; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/mybatis/dynamic/sql/where/package-info.java b/src/main/java/org/mybatis/dynamic/sql/where/package-info.java new file mode 100644 index 000000000..194b40e86 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/where/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +@NullMarked +package org.mybatis.dynamic.sql.where; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/mybatis/dynamic/sql/where/render/ColumnAndConditionRenderer.java b/src/main/java/org/mybatis/dynamic/sql/where/render/ColumnAndConditionRenderer.java new file mode 100644 index 000000000..c094bca1b --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/where/render/ColumnAndConditionRenderer.java @@ -0,0 +1,70 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.where.render; + +import java.util.Objects; +import java.util.stream.Collectors; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.RenderableCondition; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.FragmentCollector; + +public class ColumnAndConditionRenderer { + private final BindableColumn column; + private final RenderableCondition condition; + private final RenderingContext renderingContext; + + private ColumnAndConditionRenderer(Builder builder) { + column = Objects.requireNonNull(builder.column); + condition = Objects.requireNonNull(builder.condition); + renderingContext = Objects.requireNonNull(builder.renderingContext); + } + + public FragmentAndParameters render() { + FragmentCollector fc = new FragmentCollector(); + fc.add(condition.renderLeftColumn(renderingContext, column)); + fc.add(condition.renderCondition(renderingContext, column)); + return fc.toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ + } + + public static class Builder { + private @Nullable BindableColumn column; + private @Nullable RenderableCondition condition; + private @Nullable RenderingContext renderingContext; + + public Builder withColumn(BindableColumn column) { + this.column = column; + return this; + } + + public Builder withCondition(RenderableCondition condition) { + this.condition = condition; + return this; + } + + public Builder withRenderingContext(RenderingContext renderingContext) { + this.renderingContext = renderingContext; + return this; + } + + public ColumnAndConditionRenderer build() { + return new ColumnAndConditionRenderer<>(this); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/where/render/CriterionRenderer.java b/src/main/java/org/mybatis/dynamic/sql/where/render/CriterionRenderer.java index 8325bff6f..09e8091c8 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/render/CriterionRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/render/CriterionRenderer.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -18,107 +18,221 @@ import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; import java.util.stream.Collectors; +import org.mybatis.dynamic.sql.AndOrCriteriaGroup; +import org.mybatis.dynamic.sql.ColumnAndConditionCriterion; +import org.mybatis.dynamic.sql.CriteriaGroup; +import org.mybatis.dynamic.sql.ExistsCriterion; +import org.mybatis.dynamic.sql.ExistsPredicate; +import org.mybatis.dynamic.sql.NotCriterion; import org.mybatis.dynamic.sql.SqlCriterion; -import org.mybatis.dynamic.sql.render.RenderingStrategy; -import org.mybatis.dynamic.sql.render.TableAliasCalculator; +import org.mybatis.dynamic.sql.SqlCriterionVisitor; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.select.render.SubQueryRenderer; import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.FragmentCollector; + +/** + * Renders a {@link SqlCriterion} to a {@link RenderedCriterion}. The process is complex because all conditions + * may or may not be a candidate for rendering. For example, "isEqualWhenPresent" will not render when the value + * is null. It is also complex because SqlCriterion may or may not include sub-criteria. + * + *

Rendering is a recursive process. The renderer will recurse into each sub-criteria - which may also + * contain further sub-criteria - until all possible sub-criteria are rendered into a single fragment. So, for example, + * the fragment may end up looking like: + * + *

+ *     col1 = ? and (col2 = ? or (col3 = ? and col4 = ?))
+ * 
+ * + *

It is also possible that the end result will be empty if all criteria and sub-criteria are not valid for + * rendering. + * + * @author Jeff Butler + */ +public class CriterionRenderer implements SqlCriterionVisitor> { + private final RenderingContext renderingContext; + + public CriterionRenderer(RenderingContext renderingContext) { + this.renderingContext = Objects.requireNonNull(renderingContext); + } + + @Override + public Optional visit(ColumnAndConditionCriterion criterion) { + Optional initialCriterion = renderColumnAndCondition(criterion); + List renderedSubCriteria = renderSubCriteria(criterion.subCriteria()); + + return initialCriterion.map(fp -> calculateRenderedCriterion(fp, renderedSubCriteria, this::calculateFragment)) + .orElseGet(() -> calculateRenderedCriterion(renderedSubCriteria, this::calculateFragment)); + } + + @Override + public Optional visit(ExistsCriterion criterion) { + FragmentAndParameters initialCriterion = renderExists(criterion); + List renderedSubCriteria = renderSubCriteria(criterion.subCriteria()); + + return calculateRenderedCriterion(initialCriterion, renderedSubCriteria, this::calculateFragment); + } + + @Override + public Optional visit(CriteriaGroup criterion) { + return renderCriteriaGroup(criterion, this::calculateFragment); + } + + @Override + public Optional visit(NotCriterion criterion) { + return renderCriteriaGroup(criterion, this::calculateNotFragment); + } + + private Optional renderCriteriaGroup(CriteriaGroup criterion, + Function fragmentCalculator) { + return criterion.initialCriterion().map(ic -> render(ic, criterion.subCriteria(), fragmentCalculator)) + .orElseGet(() -> render(criterion.subCriteria(), fragmentCalculator)); + } + + public Optional render(SqlCriterion initialCriterion, List subCriteria, + Function fragmentCalculator) { + Optional fragmentAndParameters = initialCriterion.accept(this) + .map(RenderedCriterion::fragmentAndParameters); + List renderedSubCriteria = renderSubCriteria(subCriteria); + + return fragmentAndParameters.map(fp -> calculateRenderedCriterion(fp, renderedSubCriteria, fragmentCalculator)) + .orElseGet(() -> calculateRenderedCriterion(renderedSubCriteria, fragmentCalculator)); + } + + public Optional render(List subCriteria, + Function fragmentCalculator) { + List renderedSubCriteria = renderSubCriteria(subCriteria); + return calculateRenderedCriterion(renderedSubCriteria, fragmentCalculator); + } + + private Optional renderColumnAndCondition(ColumnAndConditionCriterion criterion) { + if (criterion.condition().shouldRender(renderingContext)) { + return Optional.of(renderCondition(criterion)); + } else { + criterion.condition().renderingSkipped(); + return Optional.empty(); + } + } + + private FragmentAndParameters renderExists(ExistsCriterion criterion) { + ExistsPredicate existsPredicate = criterion.existsPredicate(); + return SubQueryRenderer.withSelectModel(existsPredicate.selectModelBuilder().build()) + .withRenderingContext(renderingContext) + .withPrefix(existsPredicate.operator() + " (") //$NON-NLS-1$ + .withSuffix(")") //$NON-NLS-1$ + .build() + .render(); + } + + private List renderSubCriteria(List subCriteria) { + return subCriteria.stream().map(this::renderAndOrCriteriaGroup) + .flatMap(Optional::stream) + .toList(); + } + + private Optional renderAndOrCriteriaGroup(AndOrCriteriaGroup criterion) { + return criterion.initialCriterion().map(ic -> render(ic, criterion.subCriteria(), this::calculateFragment)) + .orElseGet(() -> render(criterion.subCriteria(), this::calculateFragment)) + .map(rc -> rc.withConnector(criterion.connector())); + } + + private Optional calculateRenderedCriterion(FragmentAndParameters initialCriterion, + List renderedSubCriteria, Function fragmentCalculator) { + return Optional.of(calculateRenderedCriterion( + collectSqlFragments(initialCriterion, renderedSubCriteria), fragmentCalculator)); + } + + private RenderedCriterion calculateRenderedCriterion(FragmentCollector fragmentCollector, + Function fragmentCalculator) { + FragmentAndParameters fragmentAndParameters = FragmentAndParameters + .withFragment(fragmentCalculator.apply(fragmentCollector)) + .withParameters(fragmentCollector.parameters()) + .build(); -public class CriterionRenderer { - private SqlCriterion sqlCriterion; - private AtomicInteger sequence; - private RenderingStrategy renderingStrategy; - private TableAliasCalculator tableAliasCalculator; - private String parameterName; - - private CriterionRenderer(Builder builder) { - sqlCriterion = Objects.requireNonNull(builder.sqlCriterion); - sequence = Objects.requireNonNull(builder.sequence); - renderingStrategy = Objects.requireNonNull(builder.renderingStrategy); - tableAliasCalculator = Objects.requireNonNull(builder.tableAliasCalculator); - parameterName = builder.parameterName; - } - - public Optional render() { - Optional initialCondition = renderCondition(); - - List subCriteria = sqlCriterion.mapSubCriteria(this::renderSubCriterion) - .filter(Optional::isPresent) - .map(Optional::get) - .collect(Collectors.toList()); - return new RenderedCriterion.Builder() - .withConnector(sqlCriterion.connector()) - .withInitialCondition(initialCondition) - .withSubCriteria(subCriteria) + .withFragmentAndParameters(fragmentAndParameters) .build(); } - private Optional renderSubCriterion(SqlCriterion subCriterion) { - return CriterionRenderer.withCriterion(subCriterion) - .withSequence(sequence) - .withRenderingStrategy(renderingStrategy) - .withTableAliasCalculator(tableAliasCalculator) - .withParameterName(parameterName) + private Optional calculateRenderedCriterion(List renderedSubCriteria, + Function fragmentCalculator) { + return collectSqlFragments(renderedSubCriteria).map(fc -> calculateRenderedCriterion(fc, fragmentCalculator)); + } + + private FragmentAndParameters renderCondition(ColumnAndConditionCriterion criterion) { + return new ColumnAndConditionRenderer.Builder() + .withColumn(criterion.column()) + .withCondition(criterion.condition()) + .withRenderingContext(renderingContext) .build() .render(); } - - private Optional renderCondition() { - if (!sqlCriterion.condition().shouldRender()) { + + /** + * This method encapsulates the logic of building a collection of fragments from an initial condition + * and a list of rendered sub criteria. In this overload we know there is an initial condition + * and there may be subcriteria. The collector will contain the initial condition and any rendered subcriteria + * in order. + * + * @param initialCondition - may not be null. If there is no initial condition, then use the other overload + * @param renderedSubCriteria - a list of previously rendered sub criteria. The sub criteria will all + * have connectors (either an AND or an OR) + * @return a fragment collector whose fragments represent the final calculated list of fragments and parameters. + * The fragment collector can be used to calculate the single composed fragment - either as a where clause, or + * a valid rendered sub criteria in the case of a recursive call. + */ + private FragmentCollector collectSqlFragments(FragmentAndParameters initialCondition, + List renderedSubCriteria) { + return renderedSubCriteria.stream() + .map(RenderedCriterion::fragmentAndParametersWithConnector) + .collect(FragmentCollector.collect(initialCondition)); + } + + /** + * This method encapsulates the logic of building a collection of fragments from a list of rendered sub criteria. + * In this overload we take the initial condition to be the first element in the subcriteria list. + * The collector will contain the rendered subcriteria in order. However, the connector from the first rendered + * sub criterion will be removed. This to avoid generating an invalid where clause like "where and a < 3" + * + * @param renderedSubCriteria - a list of previously rendered sub criteria. The sub criteria will all + * have connectors (either an AND or an OR) + * @return a fragment collector whose fragments represent the final calculated list of fragments and parameters. + * The fragment collector can be used to calculate the single composed fragment - either as a where clause, or + * a valid rendered sub criteria in the case of a recursive call. + */ + private Optional collectSqlFragments(List renderedSubCriteria) { + if (renderedSubCriteria.isEmpty()) { return Optional.empty(); } - WhereConditionVisitor visitor = WhereConditionVisitor.withColumn(sqlCriterion.column()) - .withRenderingStrategy(renderingStrategy) - .withSequence(sequence) - .withTableAliasCalculator(tableAliasCalculator) - .withParameterName(parameterName) - .build(); - return sqlCriterion.condition().accept(visitor); - } - - public static Builder withCriterion(SqlCriterion sqlCriterion) { - return new Builder().withCriterion(sqlCriterion); - } - - public static class Builder { - private SqlCriterion sqlCriterion; - private AtomicInteger sequence; - private RenderingStrategy renderingStrategy; - private TableAliasCalculator tableAliasCalculator; - private String parameterName; - - public Builder withCriterion(SqlCriterion sqlCriterion) { - this.sqlCriterion = sqlCriterion; - return this; - - } - - public Builder withSequence(AtomicInteger sequence) { - this.sequence = sequence; - return this; - } - - public Builder withRenderingStrategy(RenderingStrategy renderingStrategy) { - this.renderingStrategy = renderingStrategy; - return this; - } - - public Builder withTableAliasCalculator(TableAliasCalculator tableAliasCalculator) { - this.tableAliasCalculator = tableAliasCalculator; - return this; - } + FragmentAndParameters firstCondition = renderedSubCriteria.get(0).fragmentAndParameters(); - public Builder withParameterName(String parameterName) { - this.parameterName = parameterName; - return this; + FragmentCollector fc = renderedSubCriteria.stream() + .skip(1) + .map(RenderedCriterion::fragmentAndParametersWithConnector) + .collect(FragmentCollector.collect(firstCondition)); + + return Optional.of(fc); + } + + private String calculateFragment(FragmentCollector collector) { + if (collector.hasMultipleFragments()) { + return collector.collectFragments( + Collectors.joining(" ", "(", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } else { + return collector.firstFragment().orElse(""); //$NON-NLS-1$ } - - public CriterionRenderer build() { - return new CriterionRenderer<>(this); + } + + private String calculateNotFragment(FragmentCollector collector) { + if (collector.hasMultipleFragments()) { + return collector.collectFragments( + Collectors.joining(" ", "not (", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } else { + return collector.firstFragment().map(s -> "not " + s).orElse(""); //$NON-NLS-1$ //$NON-NLS-2$ } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/render/DefaultWhereClauseProvider.java b/src/main/java/org/mybatis/dynamic/sql/where/render/DefaultWhereClauseProvider.java new file mode 100644 index 000000000..5a10f77d7 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/where/render/DefaultWhereClauseProvider.java @@ -0,0 +1,65 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.where.render; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.jspecify.annotations.Nullable; + +public class DefaultWhereClauseProvider implements WhereClauseProvider { + private final String whereClause; + private final Map parameters; + + private DefaultWhereClauseProvider(Builder builder) { + whereClause = Objects.requireNonNull(builder.whereClause); + parameters = builder.parameters; + } + + @Override + public Map getParameters() { + return parameters; + } + + @Override + public String getWhereClause() { + return whereClause; + } + + public static Builder withWhereClause(String whereClause) { + return new Builder().withWhereClause(whereClause); + } + + public static class Builder { + private @Nullable String whereClause; + private final Map parameters = new HashMap<>(); + + public Builder withWhereClause(String whereClause) { + this.whereClause = whereClause; + return this; + } + + public Builder withParameters(Map parameters) { + this.parameters.putAll(parameters); + return this; + } + + public DefaultWhereClauseProvider build() { + return new DefaultWhereClauseProvider(this); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/where/render/RenderedCriterion.java b/src/main/java/org/mybatis/dynamic/sql/where/render/RenderedCriterion.java index 0c0035ca4..1c6b54f27 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/render/RenderedCriterion.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/render/RenderedCriterion.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,97 +15,61 @@ */ package org.mybatis.dynamic.sql.where.render; -import java.util.ArrayList; -import java.util.List; +import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore; + import java.util.Objects; -import java.util.Optional; -import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; import org.mybatis.dynamic.sql.util.FragmentAndParameters; -import org.mybatis.dynamic.sql.util.FragmentCollector; public class RenderedCriterion { - private Optional connector; - private Optional initialCondition; - private List subCriteria; - + private final @Nullable String connector; + private final FragmentAndParameters fragmentAndParameters; + private RenderedCriterion(Builder builder) { - connector = Objects.requireNonNull(builder.connector); - initialCondition = Objects.requireNonNull(builder.initialCondition); - subCriteria = Objects.requireNonNull(builder.subCriteria); + connector = builder.connector; + fragmentAndParameters = Objects.requireNonNull(builder.fragmentAndParameters); } - public FragmentAndParameters renderWithInitialConnector() { - FragmentAndParameters fp = renderWithoutInitialConnector(); - - return connector.map(fp::prependFragment).orElse(fp); + public FragmentAndParameters fragmentAndParameters() { + return fragmentAndParameters; } - - public FragmentAndParameters renderWithoutInitialConnector() { - FragmentCollector fc = internalRender(); - String fragment = calculateFragment(fc); - - return FragmentAndParameters.withFragment(fragment) - .withParameters(fc.parameters()) - .build(); - } - - private FragmentCollector internalRender() { - return initialCondition.map(this::renderConditionAndSubCriteria) - .orElseGet(this::renderSubCriteriaOnly); + public FragmentAndParameters fragmentAndParametersWithConnector() { + if (connector == null) { + return fragmentAndParameters; + } else { + return prependFragment(fragmentAndParameters, connector); + } } - private FragmentCollector renderSubCriteriaOnly() { - FragmentAndParameters initial = subCriteria.get(0).renderWithoutInitialConnector(); - - return subCriteria.stream() - .skip(1) - .map(RenderedCriterion::renderWithInitialConnector) - .collect(FragmentCollector.collect(initial)); + public RenderedCriterion withConnector(String connector) { + return new RenderedCriterion.Builder() + .withFragmentAndParameters(fragmentAndParameters) + .withConnector(connector) + .build(); } - private FragmentCollector renderConditionAndSubCriteria(FragmentAndParameters initialCondition) { - return subCriteria.stream() - .map(RenderedCriterion::renderWithInitialConnector) - .collect(FragmentCollector.collect(initialCondition)); + private FragmentAndParameters prependFragment(FragmentAndParameters fragmentAndParameters, String connector) { + return fragmentAndParameters.mapFragment(s -> connector + spaceBefore(s)); } - private String calculateFragment(FragmentCollector collector) { - if (collector.hasMultipleFragments()) { - return collector.fragments() - .collect(Collectors.joining(" ", "(", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - } else { - return collector.fragments().findFirst().orElse(""); //$NON-NLS-1$ - } - } - public static class Builder { - private Optional connector = Optional.empty(); - private Optional initialCondition = Optional.empty(); - private List subCriteria = new ArrayList<>(); + private @Nullable String connector; + private @Nullable FragmentAndParameters fragmentAndParameters; - public Builder withConnector(Optional connector) { + public Builder withConnector(String connector) { this.connector = connector; return this; } - - public Builder withInitialCondition(Optional initialCondition) { - this.initialCondition = initialCondition; - return this; - } - - public Builder withSubCriteria(List subCriteria) { - this.subCriteria.addAll(subCriteria); + + public Builder withFragmentAndParameters(FragmentAndParameters fragmentAndParameters) { + this.fragmentAndParameters = fragmentAndParameters; return this; } - - public Optional build() { - if (!initialCondition.isPresent() && subCriteria.isEmpty()) { - return Optional.empty(); - } - return Optional.of(new RenderedCriterion(this)); + public RenderedCriterion build() { + return new RenderedCriterion(this); } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/render/WhereClauseProvider.java b/src/main/java/org/mybatis/dynamic/sql/where/render/WhereClauseProvider.java index 10056597e..6fab1f340 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/render/WhereClauseProvider.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/render/WhereClauseProvider.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2017 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,48 +15,10 @@ */ package org.mybatis.dynamic.sql.where.render; -import java.util.Collections; -import java.util.HashMap; import java.util.Map; -import java.util.Objects; -public class WhereClauseProvider { - private String whereClause; - private Map parameters; +public interface WhereClauseProvider { + Map getParameters(); - private WhereClauseProvider(Builder builder) { - whereClause = Objects.requireNonNull(builder.whereClause); - parameters = Objects.requireNonNull(builder.parameters); - } - - public Map getParameters() { - return Collections.unmodifiableMap(parameters); - } - - public String getWhereClause() { - return whereClause; - } - - public static Builder withWhereClause(String whereClause) { - return new Builder().withWhereClause(whereClause); - } - - public static class Builder { - private String whereClause; - private Map parameters = new HashMap<>(); - - public Builder withWhereClause(String whereClause) { - this.whereClause = whereClause; - return this; - } - - public Builder withParameters(Map parameters) { - this.parameters.putAll(parameters); - return this; - } - - public WhereClauseProvider build() { - return new WhereClauseProvider(this); - } - } + String getWhereClause(); } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/render/WhereConditionVisitor.java b/src/main/java/org/mybatis/dynamic/sql/where/render/WhereConditionVisitor.java deleted file mode 100644 index 622a894ff..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/where/render/WhereConditionVisitor.java +++ /dev/null @@ -1,178 +0,0 @@ -/** - * Copyright 2016-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.where.render; - -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicInteger; - -import org.mybatis.dynamic.sql.AbstractColumnComparisonCondition; -import org.mybatis.dynamic.sql.AbstractListValueCondition; -import org.mybatis.dynamic.sql.AbstractNoValueCondition; -import org.mybatis.dynamic.sql.AbstractSingleValueCondition; -import org.mybatis.dynamic.sql.AbstractSubselectCondition; -import org.mybatis.dynamic.sql.AbstractTwoValueCondition; -import org.mybatis.dynamic.sql.BindableColumn; -import org.mybatis.dynamic.sql.ConditionVisitor; -import org.mybatis.dynamic.sql.render.RenderingStrategy; -import org.mybatis.dynamic.sql.render.TableAliasCalculator; -import org.mybatis.dynamic.sql.select.render.SelectRenderer; -import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; -import org.mybatis.dynamic.sql.util.FragmentAndParameters; -import org.mybatis.dynamic.sql.util.FragmentCollector; - -public class WhereConditionVisitor implements ConditionVisitor> { - - private RenderingStrategy renderingStrategy; - private AtomicInteger sequence; - private BindableColumn column; - private TableAliasCalculator tableAliasCalculator; - private String parameterPrefix; - - private WhereConditionVisitor(Builder builder) { - renderingStrategy = Objects.requireNonNull(builder.renderingStrategy); - sequence = Objects.requireNonNull(builder.sequence); - column = Objects.requireNonNull(builder.column); - tableAliasCalculator = Objects.requireNonNull(builder.tableAliasCalculator); - parameterPrefix = Objects.requireNonNull(builder.parameterPrefix); - } - - @Override - public Optional visit(AbstractListValueCondition condition) { - FragmentCollector fc = condition.mapValues(this::toFragmentAndParameters) - .collect(FragmentCollector.collect()); - - if (fc.isEmpty()) { - return Optional.empty(); - } - - return FragmentAndParameters.withFragment(condition.renderCondition(columnName(), fc.fragments())) - .withParameters(fc.parameters()) - .buildOptional(); - } - - @Override - public Optional visit(AbstractNoValueCondition condition) { - return FragmentAndParameters.withFragment(condition.renderCondition(columnName())) - .buildOptional(); - } - - @Override - public Optional visit(AbstractSingleValueCondition condition) { - String mapKey = RenderingStrategy.formatParameterMapKey(sequence); - String fragment = condition.renderCondition(columnName(), - getFormattedJdbcPlaceholder(mapKey)); - - return FragmentAndParameters.withFragment(fragment) - .withParameter(mapKey, condition.value()) - .buildOptional(); - } - - @Override - public Optional visit(AbstractTwoValueCondition condition) { - String mapKey1 = RenderingStrategy.formatParameterMapKey(sequence); - String mapKey2 = RenderingStrategy.formatParameterMapKey(sequence); - String fragment = condition.renderCondition(columnName(), - getFormattedJdbcPlaceholder(mapKey1), - getFormattedJdbcPlaceholder(mapKey2)); - - return FragmentAndParameters.withFragment(fragment) - .withParameter(mapKey1, condition.value1()) - .withParameter(mapKey2, condition.value2()) - .buildOptional(); - } - - - @Override - public Optional visit(AbstractSubselectCondition condition) { - SelectStatementProvider selectStatement = SelectRenderer.withSelectModel(condition.selectModel()) - .withRenderingStrategy(renderingStrategy) - .withSequence(sequence) - .build() - .render(); - - String fragment = condition.renderCondition(columnName(), selectStatement.getSelectStatement()); - - return FragmentAndParameters.withFragment(fragment) - .withParameters(selectStatement.getParameters()) - .buildOptional(); - } - - @Override - public Optional visit(AbstractColumnComparisonCondition condition) { - String fragment = condition.renderCondition(columnName(), tableAliasCalculator); - return FragmentAndParameters.withFragment(fragment).buildOptional(); - } - - private FragmentAndParameters toFragmentAndParameters(T value) { - String mapKey = RenderingStrategy.formatParameterMapKey(sequence); - - return FragmentAndParameters.withFragment(getFormattedJdbcPlaceholder(mapKey)) - .withParameter(mapKey, value) - .build(); - } - - private String getFormattedJdbcPlaceholder(String mapKey) { - return renderingStrategy.getFormattedJdbcPlaceholder(column, parameterPrefix, mapKey); - } - - private String columnName() { - return column.renderWithTableAlias(tableAliasCalculator); - } - - public static Builder withColumn(BindableColumn column) { - return new Builder().withColumn(column); - } - - public static class Builder { - private RenderingStrategy renderingStrategy; - private AtomicInteger sequence; - private BindableColumn column; - private TableAliasCalculator tableAliasCalculator; - private String parameterPrefix = RenderingStrategy.DEFAULT_PARAMETER_PREFIX; - - public Builder withSequence(AtomicInteger sequence) { - this.sequence = sequence; - return this; - } - - public Builder withRenderingStrategy(RenderingStrategy renderingStrategy) { - this.renderingStrategy = renderingStrategy; - return this; - } - - public Builder withColumn(BindableColumn column) { - this.column = column; - return this; - } - - public Builder withTableAliasCalculator(TableAliasCalculator tableAliasCalculator) { - this.tableAliasCalculator = tableAliasCalculator; - return this; - } - - public Builder withParameterName(String parameterName) { - if (parameterName != null) { - parameterPrefix = parameterName + "." + RenderingStrategy.DEFAULT_PARAMETER_PREFIX; //$NON-NLS-1$ - } - return this; - } - - public WhereConditionVisitor build() { - return new WhereConditionVisitor<>(this); - } - } -} diff --git a/src/main/java/org/mybatis/dynamic/sql/where/render/WhereRenderer.java b/src/main/java/org/mybatis/dynamic/sql/where/render/WhereRenderer.java index bfdafb1c2..0db5f2dc0 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/render/WhereRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/render/WhereRenderer.java @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2018 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,110 +15,43 @@ */ package org.mybatis.dynamic.sql.where.render; -import java.util.List; -import java.util.Objects; import java.util.Optional; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; -import org.mybatis.dynamic.sql.SqlCriterion; -import org.mybatis.dynamic.sql.render.RenderingStrategy; -import org.mybatis.dynamic.sql.render.TableAliasCalculator; +import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionModel; +import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionRenderer; +import org.mybatis.dynamic.sql.exception.NonRenderingWhereClauseException; import org.mybatis.dynamic.sql.util.FragmentAndParameters; -import org.mybatis.dynamic.sql.util.FragmentCollector; -import org.mybatis.dynamic.sql.where.WhereModel; -public class WhereRenderer { - private WhereModel whereModel; - private AtomicInteger sequence; - private RenderingStrategy renderingStrategy; - private TableAliasCalculator tableAliasCalculator; - private String parameterName; - +public class WhereRenderer extends AbstractBooleanExpressionRenderer { private WhereRenderer(Builder builder) { - whereModel = Objects.requireNonNull(builder.whereModel); - sequence = Objects.requireNonNull(builder.sequence); - renderingStrategy = Objects.requireNonNull(builder.renderingStrategy); - tableAliasCalculator = Objects.requireNonNull(builder.tableAliasCalculator); - parameterName = builder.parameterName; + super("where", builder); //$NON-NLS-1$ } - - public Optional render() { - List renderedCriteria = whereModel.mapCriteria(this::render) - .filter(Optional::isPresent) - .map(Optional::get) - .collect(Collectors.toList()); - - if (renderedCriteria.isEmpty()) { - return Optional.empty(); - } - // The first is rendered without the initial connector because we don't want something like - // where and(id = ?). This can happen if the first condition doesn't render. - FragmentAndParameters initialCriterion = renderedCriteria.get(0).renderWithoutInitialConnector(); - - FragmentCollector fc = renderedCriteria.stream() - .skip(1) - .map(RenderedCriterion::renderWithInitialConnector) - .collect(FragmentCollector.collect(initialCriterion)); - - WhereClauseProvider wcp = WhereClauseProvider.withWhereClause(calculateWhereClause(fc)) - .withParameters(fc.parameters()) - .build(); - return Optional.of(wcp); - } - - private Optional render(SqlCriterion criterion) { - return CriterionRenderer.withCriterion(criterion) - .withSequence(sequence) - .withRenderingStrategy(renderingStrategy) - .withTableAliasCalculator(tableAliasCalculator) - .withParameterName(parameterName) - .build() - .render(); - } - - private String calculateWhereClause(FragmentCollector collector) { - return collector.fragments() - .collect(Collectors.joining(" ", "where ", "")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + @Override + public Optional render() { + Optional whereClause = super.render(); + + if (whereClause.isPresent() || renderingContext.isNonRenderingClauseAllowed()) { + return whereClause; + } else { + throw new NonRenderingWhereClauseException(); + } } - - public static Builder withWhereModel(WhereModel whereModel) { - return new Builder().withWhereModel(whereModel); + + public static Builder withWhereModel(AbstractBooleanExpressionModel whereModel) { + return new Builder(whereModel); } - - public static class Builder { - private WhereModel whereModel; - private RenderingStrategy renderingStrategy; - private TableAliasCalculator tableAliasCalculator; - private AtomicInteger sequence; - private String parameterName; - - public Builder withWhereModel(WhereModel whereModel) { - this.whereModel = whereModel; - return this; - } - - public Builder withRenderingStrategy(RenderingStrategy renderingStrategy) { - this.renderingStrategy = renderingStrategy; - return this; - } - - public Builder withTableAliasCalculator(TableAliasCalculator tableAliasCalculator) { - this.tableAliasCalculator = tableAliasCalculator; - return this; - } - - public Builder withSequence(AtomicInteger sequence) { - this.sequence = sequence; - return this; + + public static class Builder extends AbstractBuilder { + public Builder(AbstractBooleanExpressionModel whereModel) { + super(whereModel); } - public Builder withParameterName(String parameterName) { - this.parameterName = parameterName; + @Override + protected Builder getThis() { return this; } - + public WhereRenderer build() { return new WhereRenderer(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/render/package-info.java b/src/main/java/org/mybatis/dynamic/sql/where/render/package-info.java new file mode 100644 index 000000000..cde1387a3 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/where/render/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +@NullMarked +package org.mybatis.dynamic.sql.where.render; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/AbstractQueryExpressionDSLExtensions.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/AbstractQueryExpressionDSLExtensions.kt deleted file mode 100644 index 5773583a1..000000000 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/AbstractQueryExpressionDSLExtensions.kt +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright 2016-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.util.kotlin - -import org.mybatis.dynamic.sql.SqlTable -import org.mybatis.dynamic.sql.select.AbstractQueryExpressionDSL -import org.mybatis.dynamic.sql.select.SelectModel - -fun > AbstractQueryExpressionDSL - .join(table: SqlTable, collect: JoinReceiver): T { - val collector = JoinCollector() - collect(collector) - return join(table, collector.onJoinCriterion, collector.andJoinCriteria) -} - -fun > AbstractQueryExpressionDSL - .join(table: SqlTable, alias: String, collect: JoinReceiver): T { - val collector = JoinCollector() - collect(collector) - return join(table, alias, collector.onJoinCriterion, collector.andJoinCriteria) -} - -fun > AbstractQueryExpressionDSL - .fullJoin(table: SqlTable, collect: JoinReceiver): T { - val collector = JoinCollector() - collect(collector) - return fullJoin(table, collector.onJoinCriterion, collector.andJoinCriteria) -} - -fun > AbstractQueryExpressionDSL - .fullJoin(table: SqlTable, alias: String, collect: JoinReceiver): T { - val collector = JoinCollector() - collect(collector) - return fullJoin(table, alias, collector.onJoinCriterion, collector.andJoinCriteria) -} - -fun > AbstractQueryExpressionDSL - .leftJoin(table: SqlTable, collect: JoinReceiver): T { - val collector = JoinCollector() - collect(collector) - return leftJoin(table, collector.onJoinCriterion, collector.andJoinCriteria) -} - -fun > AbstractQueryExpressionDSL - .leftJoin(table: SqlTable, alias: String, collect: JoinReceiver): T { - val collector = JoinCollector() - collect(collector) - return leftJoin(table, alias, collector.onJoinCriterion, collector.andJoinCriteria) -} - -fun > AbstractQueryExpressionDSL - .rightJoin(table: SqlTable, collect: JoinReceiver): T { - val collector = JoinCollector() - collect(collector) - return rightJoin(table, collector.onJoinCriterion, collector.andJoinCriteria) -} - -fun > AbstractQueryExpressionDSL - .rightJoin(table: SqlTable, alias: String, collect: JoinReceiver): T { - val collector = JoinCollector() - collect(collector) - return rightJoin(table, alias, collector.onJoinCriterion, collector.andJoinCriteria) -} diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/AbstractWhereDSLExtensions.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/AbstractWhereDSLExtensions.kt deleted file mode 100644 index 5d1957c72..000000000 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/AbstractWhereDSLExtensions.kt +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright 2016-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.util.kotlin - -import org.mybatis.dynamic.sql.BindableColumn -import org.mybatis.dynamic.sql.VisitableCondition -import org.mybatis.dynamic.sql.where.AbstractWhereDSL - -typealias WhereApplier = AbstractWhereDSL<*>.() -> AbstractWhereDSL<*> - -fun > AbstractWhereDSL.where(column: BindableColumn, condition: VisitableCondition, collect: CriteriaReceiver): M { - val collector = CriteriaCollector() - collect(collector) - return where(column, condition, collector.criteria) -} - -fun > AbstractWhereDSL.and(column: BindableColumn, condition: VisitableCondition, collect: CriteriaReceiver): M { - val collector = CriteriaCollector() - collect(collector) - return and(column, condition, collector.criteria) -} - -fun > AbstractWhereDSL.or(column: BindableColumn, condition: VisitableCondition, collect: CriteriaReceiver): M { - val collector = CriteriaCollector() - collect(collector) - return or(column, condition, collector.criteria) -} diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/CriteriaCollector.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/CriteriaCollector.kt deleted file mode 100644 index 14b342824..000000000 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/CriteriaCollector.kt +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright 2016-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.util.kotlin - -import org.mybatis.dynamic.sql.BindableColumn -import org.mybatis.dynamic.sql.SqlCriterion -import org.mybatis.dynamic.sql.VisitableCondition - -typealias CriteriaReceiver = CriteriaCollector.() -> CriteriaCollector - -class CriteriaCollector { - val criteria = mutableListOf>() - - fun and(column: BindableColumn, condition: VisitableCondition) = - apply { - criteria.add(SqlCriterion.withColumn(column) - .withCondition(condition) - .withConnector("and") - .build()) - } - - fun and(column: BindableColumn, condition: VisitableCondition, collect: CriteriaCollector.() -> CriteriaCollector) = - apply { - val collector = CriteriaCollector() - collect(collector) - val criterion: SqlCriterion = SqlCriterion.withColumn(column) - .withCondition(condition) - .withSubCriteria(collector.criteria) - .withConnector("and") - .build() - criteria.add(criterion) - } - - fun or(column: BindableColumn, condition: VisitableCondition) = - apply { - criteria.add(SqlCriterion.withColumn(column) - .withCondition(condition) - .withConnector("or") - .build()) - } - - fun or(column: BindableColumn, condition: VisitableCondition, collect: CriteriaCollector.() -> CriteriaCollector) = - apply { - val collector = CriteriaCollector() - collect(collector) - val criterion: SqlCriterion = SqlCriterion.withColumn(column) - .withCondition(condition) - .withSubCriteria(collector.criteria) - .withConnector("or") - .build() - criteria.add(criterion) - } -} diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/FromGathererExtensions.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/FromGathererExtensions.kt deleted file mode 100644 index a7943be7b..000000000 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/FromGathererExtensions.kt +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright 2016-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.util.kotlin - -import org.mybatis.dynamic.sql.SqlTable -import org.mybatis.dynamic.sql.select.QueryExpressionDSL -import org.mybatis.dynamic.sql.select.SelectModel - -typealias QueryExpressionEnhancer = QueryExpressionDSL.() -> QueryExpressionDSL - -// These functions are intended for use in a Join mapper where a join is setup before the remainder -// of the query is completed -fun QueryExpressionDSL.FromGatherer.from(table: SqlTable, enhancer: QueryExpressionEnhancer) = - enhancer(from(table)) - -fun QueryExpressionDSL.FromGatherer.from(table: SqlTable, alias: String, enhancer: QueryExpressionEnhancer) = - enhancer(from(table, alias)) diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/GroupingCriteriaCollector.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/GroupingCriteriaCollector.kt new file mode 100644 index 000000000..a83530154 --- /dev/null +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/GroupingCriteriaCollector.kt @@ -0,0 +1,446 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util.kotlin + +import org.mybatis.dynamic.sql.AndOrCriteriaGroup +import org.mybatis.dynamic.sql.BasicColumn +import org.mybatis.dynamic.sql.BindableColumn +import org.mybatis.dynamic.sql.ColumnAndConditionCriterion +import org.mybatis.dynamic.sql.CriteriaGroup +import org.mybatis.dynamic.sql.ExistsCriterion +import org.mybatis.dynamic.sql.NotCriterion +import org.mybatis.dynamic.sql.RenderableCondition +import org.mybatis.dynamic.sql.SqlBuilder +import org.mybatis.dynamic.sql.SqlCriterion + +typealias GroupingCriteriaReceiver = GroupingCriteriaCollector.() -> Unit + +fun GroupingCriteriaReceiver.andThen(after: SubCriteriaCollector.() -> Unit): GroupingCriteriaReceiver = { + invoke(this) + after(this) +} + +@MyBatisDslMarker +sealed class SubCriteriaCollector { + internal val subCriteria = mutableListOf() + + /** + * Add sub criterion joined with "and" to the current context. If the receiver adds more than one + * criterion that renders then parentheses will be added. + * + * This function may be called multiple times in a context. + * + * @param criteriaReceiver a function to create the contained criteria + */ + fun and(criteriaReceiver: GroupingCriteriaReceiver): Unit = + GroupingCriteriaCollector().apply(criteriaReceiver).let { + subCriteria.add( + AndOrCriteriaGroup.Builder().withConnector("and") //$NON-NLS-1$ + .withInitialCriterion(it.initialCriterion) + .withSubCriteria(it.subCriteria) + .build() + ) + } + + /** + * Add a list of criteria joined with "and" to the current context. If the list contains more than + * one criterion that renders then parentheses will be added. This function is distinguished from the + * other overload in that it can accept a pre-created list of criteria and does not require any criterion + * to be the initial criterion. The first criterion that renders will be rendered without the "and" or "or". + * + * This function may be called multiple times in a context. + * + * @param criteria a list of pre-created criteria + * + */ + fun and(criteria: List) { + subCriteria.add( + AndOrCriteriaGroup.Builder().withConnector("and") //$NON-NLS-1$ + .withSubCriteria(criteria) + .build() + ) + } + + /** + * Add sub criterion joined with "or" to the current context. If the receiver adds more than one + * criterion that renders then parentheses will be added. + * + * This function may be called multiple times in a context. + * + * @param criteriaReceiver a function to create the contained criteria + */ + fun or(criteriaReceiver: GroupingCriteriaReceiver): Unit = + GroupingCriteriaCollector().apply(criteriaReceiver).let { + subCriteria.add( + AndOrCriteriaGroup.Builder().withConnector("or") //$NON-NLS-1$ + .withInitialCriterion(it.initialCriterion) + .withSubCriteria(it.subCriteria) + .build() + ) + } + + /** + * Add a list of criteria joined with "or" to the current context. If the list contains more than + * one criterion that renders then parentheses will be added. This function is distinguished from the + * other overload in that it can accept a pre-created list of criteria and does not require any criterion + * to be the initial criterion. The first criterion that renders will be rendered without the "and" or "or". + * + * This function may be called multiple times in a context. + * + * @param criteria a list of pre-created criteria + * + */ + fun or(criteria: List) { + subCriteria.add( + AndOrCriteriaGroup.Builder().withConnector("or") //$NON-NLS-1$ + .withSubCriteria(criteria) + .build() + ) + } +} + +/** + * This class is used to gather criteria for a having or where clause. The class gathers two types of criteria: + * an initial criterion, and sub-criteria connected by either an "and" or an "or". + * + * An initial criterion can be one of four types: + * - A column and condition (called with the invoke operator on a column, or an infix function) + * - An exists operator (called with the "exists" function) + * - A criteria group which is essentially parenthesis within the where clause (called with the "group" function) + * - A criteria group preceded with "not" (called with the "not" function) + * + * Only one of the initial criterion functions should be called within each scope. If you need more than one, + * use a sub-criterion joined with "and" or "or" + */ +@Suppress("TooManyFunctions") +@MyBatisDslMarker +open class GroupingCriteriaCollector : SubCriteriaCollector() { + internal var initialCriterion: SqlCriterion? = null + private set(value) { + assertNull(field, "ERROR.21") //$NON-NLS-1$ + field = value + } + + /** + * Add an initial criterion preceded with "not" to the current context. If the receiver adds more than one + * criterion that renders then parentheses will be added. + * + * This may only be called once per scope, and cannot be combined with "exists", "group", "invoke", + * or any infix function in the same scope. + * + * @param criteriaReceiver a function to create the contained criteria + */ + fun not(criteriaReceiver: GroupingCriteriaReceiver): Unit = + GroupingCriteriaCollector().apply(criteriaReceiver).let { + initialCriterion = NotCriterion.Builder() + .withInitialCriterion(it.initialCriterion) + .withSubCriteria(it.subCriteria) + .build() + } + + /** + * Add an initial criterion preceded with "not" to the current context. If the list contains more than + * one criterion that renders then parentheses will be added. This function is distinguished from the + * other overload in that it can accept a pre-created list of criteria and does not require any criterion + * to be the initial criterion. The first criterion that renders will be rendered without the "and" or "or". + * + * This may only be called once per scope, and cannot be combined with "exists", "group", "invoke", + * or any infix function in the same scope. + * + * @param criteria a list of pre-created criteria + * + */ + fun not(criteria: List) { + initialCriterion = NotCriterion.Builder().withSubCriteria(criteria).build() + } + + /** + * Add an initial criterion composed of a sub-query preceded with "exists" to the current context. + * + * This should only be specified once per scope, and cannot be combined with "invoke", + * "group", "not", or any infix function in the same scope. + * + * @param kotlinSubQueryBuilder a function to create a select statement + */ + fun exists(kotlinSubQueryBuilder: KotlinSubQueryBuilder.() -> Unit): Unit = + KotlinSubQueryBuilder().apply(kotlinSubQueryBuilder).let { + initialCriterion = ExistsCriterion.Builder().withExistsPredicate(SqlBuilder.exists(it)).build() + } + + /** + * Add an initial criterion to the current context. If the receiver adds more than one + * criterion that renders at runtime then parentheses will be added. + * + * This may only be specified once per scope, and cannot be combined with "exists", "invoke", + * "not", or any infix function in the same scope. + * + * This could "almost" be an operator invoke function. The problem is that + * to call it a user would need to use "this" explicitly. We think that is too + * confusing, so we'll stick with the function name of "group" + * + * @param criteriaReceiver a function to create the contained criteria + */ + fun group(criteriaReceiver: GroupingCriteriaReceiver): Unit = + GroupingCriteriaCollector().apply(criteriaReceiver).let { + initialCriterion = CriteriaGroup.Builder() + .withInitialCriterion(it.initialCriterion) + .withSubCriteria(it.subCriteria) + .build() + } + + /** + * Add an initial criterion preceded to the current context. If the list contains more than + * one criterion that renders then parentheses will be added. This function is distinguished from the + * other overload in that it can accept a pre-created list of criteria and does not require any criterion + * to be the initial criterion. The first criterion that renders will be rendered without the "and" or "or". + * + * This may only be specified once per scope, and cannot be combined with "exists", "invoke", + * "not", or any infix function in the same scope. + * + * @param criteria a list of pre-created criteria + * + */ + fun group(criteria: List) { + initialCriterion = CriteriaGroup.Builder().withSubCriteria(criteria).build() + } + + /** + * Add an initial criterion to the current context based on a column and condition. + * You can use it like "A.invoke(isEqualTo(3))" or "A (isEqualTo(3))". + * + * This is an extension function to a BindableColumn, but is scoped to the context of the + * current collector. + * + * This should only be specified once per scope, and cannot be combined with "exists", "group", + * "not", or any infix function in the same scope. + * + * @param condition the condition to be applied to this column, in this scope + */ + operator fun BindableColumn.invoke(condition: RenderableCondition) { + initialCriterion = ColumnAndConditionCriterion.withColumn(this) + .withCondition(condition) + .build() + } + + // infix functions...we may be able to rewrite these as extension functions once Kotlin implements the context + // parameters proposal (https://github.com/Kotlin/KEEP/issues/367) + + // conditions for all data types + fun BindableColumn<*>.isNull() = invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNull()) + + fun BindableColumn<*>.isNotNull() = invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotNull()) + + infix fun BindableColumn.isEqualTo(value: T) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isEqualTo(value)) + + infix fun BindableColumn<*>.isEqualTo(subQuery: KotlinSubQueryBuilder.() -> Unit) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isEqualTo(subQuery)) + + infix fun BindableColumn<*>.isEqualTo(column: BasicColumn) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isEqualTo(column)) + + infix fun BindableColumn.isEqualToWhenPresent(value: T?) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isEqualToWhenPresent(value)) + + infix fun BindableColumn.isNotEqualTo(value: T) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotEqualTo(value)) + + infix fun BindableColumn<*>.isNotEqualTo(subQuery: KotlinSubQueryBuilder.() -> Unit) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotEqualTo(subQuery)) + + infix fun BindableColumn<*>.isNotEqualTo(column: BasicColumn) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotEqualTo(column)) + + infix fun BindableColumn.isNotEqualToWhenPresent(value: T?) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotEqualToWhenPresent(value)) + + infix fun BindableColumn.isGreaterThan(value: T) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isGreaterThan(value)) + + infix fun BindableColumn<*>.isGreaterThan(subQuery: KotlinSubQueryBuilder.() -> Unit) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isGreaterThan(subQuery)) + + infix fun BindableColumn<*>.isGreaterThan(column: BasicColumn) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isGreaterThan(column)) + + infix fun BindableColumn.isGreaterThanWhenPresent(value: T?) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isGreaterThanWhenPresent(value)) + + infix fun BindableColumn.isGreaterThanOrEqualTo(value: T) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isGreaterThanOrEqualTo(value)) + + infix fun BindableColumn<*>.isGreaterThanOrEqualTo(subQuery: KotlinSubQueryBuilder.() -> Unit) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isGreaterThanOrEqualTo(subQuery)) + + infix fun BindableColumn<*>.isGreaterThanOrEqualTo(column: BasicColumn) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isGreaterThanOrEqualTo(column)) + + infix fun BindableColumn.isGreaterThanOrEqualToWhenPresent(value: T?) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isGreaterThanOrEqualToWhenPresent(value)) + + infix fun BindableColumn.isLessThan(value: T) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLessThan(value)) + + infix fun BindableColumn<*>.isLessThan(subQuery: KotlinSubQueryBuilder.() -> Unit) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLessThan(subQuery)) + + infix fun BindableColumn<*>.isLessThan(column: BasicColumn) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLessThan(column)) + + infix fun BindableColumn.isLessThanWhenPresent(value: T?) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLessThanWhenPresent(value)) + + infix fun BindableColumn.isLessThanOrEqualTo(value: T) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLessThanOrEqualTo(value)) + + infix fun BindableColumn<*>.isLessThanOrEqualTo(subQuery: KotlinSubQueryBuilder.() -> Unit) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLessThanOrEqualTo(subQuery)) + + infix fun BindableColumn<*>.isLessThanOrEqualTo(column: BasicColumn) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLessThanOrEqualTo(column)) + + infix fun BindableColumn.isLessThanOrEqualToWhenPresent(value: T?) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLessThanOrEqualToWhenPresent(value)) + + fun BindableColumn.isIn(vararg values: T) = isIn(values.asList()) + + infix fun BindableColumn.isIn(values: Collection) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isIn(values)) + + infix fun BindableColumn<*>.isIn(subQuery: KotlinSubQueryBuilder.() -> Unit) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isIn(subQuery)) + + fun BindableColumn.isInWhenPresent(vararg values: T?) = isInWhenPresent(values.asList()) + + infix fun BindableColumn.isInWhenPresent(values: Collection?) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isInWhenPresent(values)) + + fun BindableColumn.isNotIn(vararg values: T) = isNotIn(values.asList()) + + infix fun BindableColumn.isNotIn(values: Collection) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotIn(values)) + + infix fun BindableColumn<*>.isNotIn(subQuery: KotlinSubQueryBuilder.() -> Unit) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotIn(subQuery)) + + fun BindableColumn.isNotInWhenPresent(vararg values: T?) = isNotInWhenPresent(values.asList()) + + infix fun BindableColumn.isNotInWhenPresent(values: Collection?) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotInWhenPresent(values)) + + infix fun BindableColumn.isBetween(value1: T) = + SecondValueCollector { + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isBetween(value1).and(it)) + } + + infix fun BindableColumn.isBetweenWhenPresent(value1: T?) = + NullableSecondValueCollector { + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isBetweenWhenPresent(value1).and(it)) + } + + infix fun BindableColumn.isNotBetween(value1: T) = + SecondValueCollector { + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotBetween(value1).and(it)) + } + + infix fun BindableColumn.isNotBetweenWhenPresent(value1: T?) = + NullableSecondValueCollector { + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotBetweenWhenPresent(value1).and(it)) + } + + // for string columns, but generic for columns with type handlers + infix fun BindableColumn.isLike(value: T) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLike(value)) + + infix fun BindableColumn.isLikeWhenPresent(value: T?) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLikeWhenPresent(value)) + + infix fun BindableColumn.isNotLike(value: T) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotLike(value)) + + infix fun BindableColumn.isNotLikeWhenPresent(value: T?) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotLikeWhenPresent(value)) + + // shortcuts for booleans + fun BindableColumn.isTrue() = isEqualTo(true) + + fun BindableColumn.isFalse() = isEqualTo(false) + + // conditions for strings only + infix fun BindableColumn.isLikeCaseInsensitive(value: String) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLikeCaseInsensitive(value)) + + infix fun BindableColumn.isLikeCaseInsensitiveWhenPresent(value: String?) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLikeCaseInsensitiveWhenPresent(value)) + + infix fun BindableColumn.isNotLikeCaseInsensitive(value: String) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotLikeCaseInsensitive(value)) + + infix fun BindableColumn.isNotLikeCaseInsensitiveWhenPresent(value: String?) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotLikeCaseInsensitiveWhenPresent(value)) + + fun BindableColumn.isInCaseInsensitive(vararg values: String) = isInCaseInsensitive(values.asList()) + + infix fun BindableColumn.isInCaseInsensitive(values: Collection) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isInCaseInsensitive(values)) + + fun BindableColumn.isInCaseInsensitiveWhenPresent(vararg values: String?) = + isInCaseInsensitiveWhenPresent(values.asList()) + + infix fun BindableColumn.isInCaseInsensitiveWhenPresent(values: Collection?) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isInCaseInsensitiveWhenPresent(values)) + + fun BindableColumn.isNotInCaseInsensitive(vararg values: String) = + isNotInCaseInsensitive(values.asList()) + + infix fun BindableColumn.isNotInCaseInsensitive(values: Collection) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotInCaseInsensitive(values)) + + fun BindableColumn.isNotInCaseInsensitiveWhenPresent(vararg values: String?) = + isNotInCaseInsensitiveWhenPresent(values.asList()) + + infix fun BindableColumn.isNotInCaseInsensitiveWhenPresent(values: Collection?) = + invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotInCaseInsensitiveWhenPresent(values)) + + companion object { + fun having(receiver: GroupingCriteriaReceiver): GroupingCriteriaReceiver = receiver + + /** + * Function for code simplification. This allows creation of an independent where clause + * that can be reused in different statements. For example: + * + * val whereClause = where { id isEqualTo 3 } + * + * val rows = countFrom(foo) { + * where(whereClause) + * } + * + * Use of this function is optional. You can also write code like this: + * + * val whereClause: GroupingCriteriaReceiver = { id isEqualTo 3 } + * + */ + fun where(receiver: GroupingCriteriaReceiver): GroupingCriteriaReceiver = receiver + } +} + +class SecondValueCollector (private val consumer: (T) -> Unit) { + infix fun and(value2: T) = consumer.invoke(value2) +} + +class NullableSecondValueCollector (private val consumer: (T?) -> Unit) { + infix fun and(value2: T?) = consumer.invoke(value2) +} diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt index 6e3e652e4..c65b37a7a 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,31 +15,31 @@ */ package org.mybatis.dynamic.sql.util.kotlin -import org.mybatis.dynamic.sql.BasicColumn -import org.mybatis.dynamic.sql.select.join.JoinCondition -import org.mybatis.dynamic.sql.select.join.JoinCriterion +import org.mybatis.dynamic.sql.BindableColumn +import org.mybatis.dynamic.sql.RenderableCondition +import org.mybatis.dynamic.sql.SqlBuilder -typealias JoinReceiver = JoinCollector.() -> JoinCollector +typealias JoinReceiver = JoinCollector.() -> Unit +@MyBatisDslMarker class JoinCollector { - lateinit var onJoinCriterion: JoinCriterion - val andJoinCriteria = mutableListOf() - - fun on(column: BasicColumn, condition: JoinCondition) = - apply { - onJoinCriterion = JoinCriterion.Builder() - .withConnector("on") - .withJoinColumn(column) - .withJoinCondition(condition) - .build() - } - - fun and(column: BasicColumn, condition: JoinCondition) = - apply { - andJoinCriteria.add(JoinCriterion.Builder() - .withConnector("and") - .withJoinColumn(column) - .withJoinCondition(condition) - .build()) - } + private val criteriaCollector = GroupingCriteriaCollector() + + internal fun initialCriterion() = invalidIfNull(criteriaCollector.initialCriterion, "ERROR.22") //$NON-NLS-1$ + internal fun subCriteria() = criteriaCollector.subCriteria + + fun on(leftColumn: BindableColumn): RightColumnCollector = RightColumnCollector { + assertNull(criteriaCollector.initialCriterion, "ERROR.45") //$NON-NLS-1$ + criteriaCollector.apply { leftColumn.invoke(it) } + } + + fun and(leftColumn: BindableColumn): RightColumnCollector = RightColumnCollector { + criteriaCollector.and { leftColumn.invoke(it) } + } +} + +class RightColumnCollector(private val joinConditionConsumer: (RenderableCondition) -> Unit) { + infix fun equalTo(rightColumn: BindableColumn) = joinConditionConsumer.invoke(SqlBuilder.isEqualTo(rightColumn)) + + infix fun equalTo(value: T) = joinConditionConsumer.invoke(SqlBuilder.isEqualTo(value)) } diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KInvalidSQLException.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KInvalidSQLException.kt new file mode 100644 index 000000000..1203b555a --- /dev/null +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KInvalidSQLException.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util.kotlin + +import org.mybatis.dynamic.sql.exception.InvalidSqlException + +/** + * This exception is thrown if the library detects misuse of the Kotlin DSL that would result in invalid SQL + */ +class KInvalidSQLException(message: String) : InvalidSqlException(message) diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KValidator.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KValidator.kt new file mode 100644 index 000000000..f9b6aa323 --- /dev/null +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KValidator.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util.kotlin + +import org.mybatis.dynamic.sql.util.Messages + +fun invalidIfNull(o: T?, messageNumber: String): T { + if (o == null) { + throw KInvalidSQLException(Messages.getString(messageNumber)) + } + return o +} + +fun assertNotNull(o: Any?, messageNumber: String) { + assertTrue(o != null, messageNumber) +} + +fun assertNull(o: Any?, messageNumber: String) { + assertTrue(o == null, messageNumber) +} + +fun assertTrue(condition: Boolean, messageNumber: String) { + if (!condition) { + throw KInvalidSQLException(Messages.getString(messageNumber)) + } +} diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBaseBuilders.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBaseBuilders.kt index 77d3135c4..ea0d29f36 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBaseBuilders.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBaseBuilders.kt @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,99 +15,224 @@ */ package org.mybatis.dynamic.sql.util.kotlin -import org.mybatis.dynamic.sql.BindableColumn +import org.mybatis.dynamic.sql.AndOrCriteriaGroup import org.mybatis.dynamic.sql.SqlTable -import org.mybatis.dynamic.sql.VisitableCondition +import org.mybatis.dynamic.sql.configuration.StatementConfiguration import org.mybatis.dynamic.sql.select.AbstractQueryExpressionDSL -import org.mybatis.dynamic.sql.select.SelectModel -import org.mybatis.dynamic.sql.util.Buildable -import org.mybatis.dynamic.sql.where.AbstractWhereDSL +import org.mybatis.dynamic.sql.where.AbstractWhereStarter -abstract class KotlinBaseBuilder, B : KotlinBaseBuilder> : Buildable { - fun where(column: BindableColumn, condition: VisitableCondition): B = - applySelf { - getWhere().where(column, condition) +@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE) +@DslMarker +annotation class MyBatisDslMarker + +@MyBatisDslMarker +@Suppress("TooManyFunctions") +abstract class KotlinBaseBuilder> { + + fun configureStatement(c: StatementConfiguration.() -> Unit) { + getDsl().configureStatement(c) + } + + fun where(criteria: GroupingCriteriaReceiver): Unit = + GroupingCriteriaCollector().apply(criteria).let { + getDsl().where(it.initialCriterion, it.subCriteria) + } + + fun where(criteria: List) { + getDsl().where(criteria) + } + + /** + * This function does nothing, but it can be used to make some code snippets more understandable. + * + * For example, to count all rows in a table you can write either of the following: + * + * val rows = countFrom(foo) { } + * + * or + * + * val rows = countFrom(foo) { allRows() } + */ + @SuppressWarnings("EmptyFunctionBlock") + fun allRows() { + // intentionally empty - this function exists for code beautification and clarity only + } + + protected abstract fun getDsl(): D +} + +@Suppress("TooManyFunctions") +abstract class KotlinBaseJoiningBuilder> : KotlinBaseBuilder() { + + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") + fun join(table: SqlTable, joinCriteria: JoinReceiver): Unit = + applyToDsl(joinCriteria) { jc -> + join(table, jc.initialCriterion(), jc.subCriteria()) } - fun where(column: BindableColumn, condition: VisitableCondition, collect: CriteriaReceiver): B = - applySelf { - getWhere().where(column, condition, collect) + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") + fun join(table: SqlTable, alias: String, joinCriteria: JoinReceiver): Unit = + applyToDsl(joinCriteria) { jc -> + join(table, alias, jc.initialCriterion(), jc.subCriteria()) } - fun applyWhere(whereApplier: WhereApplier): B = - applySelf { - getWhere().applyWhere(whereApplier) + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") + fun join( + subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit, + joinCriteria: JoinReceiver + ): Unit = + applyToDsl(subQuery, joinCriteria) { sq, jc -> + join(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria()) } - fun and(column: BindableColumn, condition: VisitableCondition): B = - applySelf { - getWhere().and(column, condition) + fun join(table: SqlTable): JoinCriteriaGatherer = + JoinCriteriaGatherer { + getDsl().join(table, it.initialCriterion, it.subCriteria) } - fun and(column: BindableColumn, condition: VisitableCondition, collect: CriteriaReceiver): B = - applySelf { - getWhere().and(column, condition, collect) + fun join(table: SqlTable, alias: String): JoinCriteriaGatherer = + JoinCriteriaGatherer { + getDsl().join(table, alias, it.initialCriterion, it.subCriteria) } - fun or(column: BindableColumn, condition: VisitableCondition): B = - applySelf { - getWhere().or(column, condition) + fun join( + subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit): JoinCriteriaGatherer = + JoinCriteriaGatherer { + val sq = KotlinQualifiedSubQueryBuilder().apply(subQuery) + getDsl().join(sq, sq.correlationName, it.initialCriterion, it.subCriteria) } - fun or(column: BindableColumn, condition: VisitableCondition, collect: CriteriaReceiver): B = - applySelf { - getWhere().or(column, condition, collect) + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") + fun fullJoin(table: SqlTable, joinCriteria: JoinReceiver): Unit = + applyToDsl(joinCriteria) { jc -> + fullJoin(table, jc.initialCriterion(), jc.subCriteria()) } - protected fun applySelf(block: B.() -> Unit): B = - self().apply { block() } + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") + fun fullJoin(table: SqlTable, alias: String, joinCriteria: JoinReceiver): Unit = + applyToDsl(joinCriteria) { jc -> + fullJoin(table, alias, jc.initialCriterion(), jc.subCriteria()) + } - protected abstract fun self(): B + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") + fun fullJoin( + subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit, + joinCriteria: JoinReceiver + ): Unit = + applyToDsl(subQuery, joinCriteria) { sq, jc -> + fullJoin(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria()) + } - protected abstract fun getWhere(): W -} + fun fullJoin(table: SqlTable): JoinCriteriaGatherer = + JoinCriteriaGatherer { + getDsl().fullJoin(table, it.initialCriterion, it.subCriteria) + } -abstract class KotlinBaseJoiningBuilder, W : AbstractWhereDSL, B : KotlinBaseJoiningBuilder>( - private val dsl: AbstractQueryExpressionDSL -) : KotlinBaseBuilder() { + fun fullJoin(table: SqlTable, alias: String): JoinCriteriaGatherer = + JoinCriteriaGatherer { + getDsl().fullJoin(table, alias, it.initialCriterion, it.subCriteria) + } - fun join(table: SqlTable, receiver: JoinReceiver): B = - applySelf { - dsl.join(table, receiver) + fun fullJoin( + subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit): JoinCriteriaGatherer = + JoinCriteriaGatherer { + val sq = KotlinQualifiedSubQueryBuilder().apply(subQuery) + getDsl().fullJoin(sq, sq.correlationName, it.initialCriterion, it.subCriteria) } - fun join(table: SqlTable, alias: String, receiver: JoinReceiver): B = - applySelf { - dsl.join(table, alias, receiver) + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") + fun leftJoin(table: SqlTable, joinCriteria: JoinReceiver): Unit = + applyToDsl(joinCriteria) { jc -> + leftJoin(table, jc.initialCriterion(), jc.subCriteria()) } - fun fullJoin(table: SqlTable, receiver: JoinReceiver): B = - applySelf { - dsl.fullJoin(table, receiver) + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") + fun leftJoin(table: SqlTable, alias: String, joinCriteria: JoinReceiver): Unit = + applyToDsl(joinCriteria) { jc -> + leftJoin(table, alias, jc.initialCriterion(), jc.subCriteria()) } - fun fullJoin(table: SqlTable, alias: String, receiver: JoinReceiver): B = - applySelf { - dsl.fullJoin(table, alias, receiver) + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") + fun leftJoin( + subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit, + joinCriteria: JoinReceiver + ): Unit = + applyToDsl(subQuery, joinCriteria) { sq, jc -> + leftJoin(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria()) } - fun leftJoin(table: SqlTable, receiver: JoinReceiver): B = - applySelf { - dsl.leftJoin(table, receiver) + fun leftJoin(table: SqlTable): JoinCriteriaGatherer = + JoinCriteriaGatherer { + getDsl().leftJoin(table, it.initialCriterion, it.subCriteria) } - fun leftJoin(table: SqlTable, alias: String, receiver: JoinReceiver): B = - applySelf { - dsl.leftJoin(table, alias, receiver) + fun leftJoin(table: SqlTable, alias: String): JoinCriteriaGatherer = + JoinCriteriaGatherer { + getDsl().leftJoin(table, alias, it.initialCriterion, it.subCriteria) } - fun rightJoin(table: SqlTable, receiver: JoinReceiver): B = - applySelf { - dsl.rightJoin(table, receiver) + fun leftJoin( + subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit): JoinCriteriaGatherer = + JoinCriteriaGatherer { + val sq = KotlinQualifiedSubQueryBuilder().apply(subQuery) + getDsl().leftJoin(sq, sq.correlationName, it.initialCriterion, it.subCriteria) } - fun rightJoin(table: SqlTable, alias: String, receiver: JoinReceiver): B = - applySelf { - dsl.rightJoin(table, alias, receiver) + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") + fun rightJoin(table: SqlTable, joinCriteria: JoinReceiver): Unit = + applyToDsl(joinCriteria) { jc -> + rightJoin(table, jc.initialCriterion(), jc.subCriteria()) + } + + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") + fun rightJoin(table: SqlTable, alias: String, joinCriteria: JoinReceiver): Unit = + applyToDsl(joinCriteria) { jc -> + rightJoin(table, alias, jc.initialCriterion(), jc.subCriteria()) + } + + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") + fun rightJoin( + subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit, + joinCriteria: JoinReceiver + ): Unit = + applyToDsl(subQuery, joinCriteria) { sq, jc -> + rightJoin(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria()) + } + + fun rightJoin(table: SqlTable): JoinCriteriaGatherer = + JoinCriteriaGatherer { + getDsl().rightJoin(table, it.initialCriterion, it.subCriteria) + } + + fun rightJoin(table: SqlTable, alias: String): JoinCriteriaGatherer = + JoinCriteriaGatherer { + getDsl().rightJoin(table, alias, it.initialCriterion, it.subCriteria) + } + + fun rightJoin( + subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit): JoinCriteriaGatherer = + JoinCriteriaGatherer { + val sq = KotlinQualifiedSubQueryBuilder().apply(subQuery) + getDsl().rightJoin(sq, sq.correlationName, it.initialCriterion, it.subCriteria) + } + + private fun applyToDsl(joinCriteria: JoinReceiver, applyJoin: D.(JoinCollector) -> Unit) { + getDsl().applyJoin(JoinCollector().apply(joinCriteria)) + } + + private fun applyToDsl( + subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit, + joinCriteria: JoinReceiver, + applyJoin: D.(KotlinQualifiedSubQueryBuilder, JoinCollector) -> Unit + ) { + getDsl().applyJoin(KotlinQualifiedSubQueryBuilder().apply(subQuery), JoinCollector().apply(joinCriteria)) + } +} + +class JoinCriteriaGatherer(private val consumer: (GroupingCriteriaCollector) -> Unit) { + infix fun on (joinCriteria: GroupingCriteriaReceiver): Unit = + with(GroupingCriteriaCollector().apply(joinCriteria)) { + assertTrue(initialCriterion != null || subCriteria.isNotEmpty(), "ERROR.22") //$NON-NLS-1$ + consumer.invoke(this) } } diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBatchInsertBuilder.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBatchInsertBuilder.kt new file mode 100644 index 000000000..fed6bf1d6 --- /dev/null +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBatchInsertBuilder.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util.kotlin + +import org.mybatis.dynamic.sql.SqlColumn +import org.mybatis.dynamic.sql.SqlTable +import org.mybatis.dynamic.sql.insert.BatchInsertDSL +import org.mybatis.dynamic.sql.insert.BatchInsertModel +import org.mybatis.dynamic.sql.util.AbstractColumnMapping +import org.mybatis.dynamic.sql.util.Buildable +import org.mybatis.dynamic.sql.util.MappedColumnMapping + +typealias KotlinBatchInsertCompleter = KotlinBatchInsertBuilder.() -> Unit + +@MyBatisDslMarker +class KotlinBatchInsertBuilder (private val rows: Collection): Buildable> { + private var table: SqlTable? = null + private val columnMappings = mutableListOf() + + fun into(table: SqlTable) { + this.table = table + } + + fun map(column: SqlColumn) = MultiRowInsertColumnMapCompleter(column) { + columnMappings.add(it) + } + + fun withMappedColumn(column: SqlColumn) { + columnMappings.add(MappedColumnMapping.of(column)) + } + + override fun build(): BatchInsertModel { + assertNotNull(table, "ERROR.23") //$NON-NLS-1$ + return with(BatchInsertDSL.Builder()) { + withRecords(rows) + withTable(table!!) + withColumnMappings(columnMappings) + build() + }.build() + } +} diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinCountBuilder.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinCountBuilder.kt index eed0caeaa..c1aadfbe9 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinCountBuilder.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinCountBuilder.kt @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,20 +15,25 @@ */ package org.mybatis.dynamic.sql.util.kotlin +import org.mybatis.dynamic.sql.SqlTable import org.mybatis.dynamic.sql.select.CountDSL import org.mybatis.dynamic.sql.select.SelectModel import org.mybatis.dynamic.sql.util.Buildable -typealias CountCompleter = KotlinCountBuilder.() -> Buildable +typealias CountCompleter = KotlinCountBuilder.() -> Unit -class KotlinCountBuilder(private val dsl: CountDSL) : - KotlinBaseJoiningBuilder, CountDSL.CountWhereBuilder, KotlinCountBuilder>(dsl) { +class KotlinCountBuilder(private val fromGatherer: CountDSL.FromGatherer) : + KotlinBaseJoiningBuilder>(), + Buildable { - fun allRows() = this + private var dsl: CountDSL? = null - override fun build(): SelectModel = dsl.build() + fun from(table: SqlTable): KotlinCountBuilder = + apply { + dsl = fromGatherer.from(table) + } - override fun getWhere(): CountDSL.CountWhereBuilder = dsl.where() + override fun build(): SelectModel = getDsl().build() - override fun self() = this + override fun getDsl(): CountDSL = invalidIfNull(dsl, "ERROR.24") //$NON-NLS-1$ } diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinDeleteBuilder.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinDeleteBuilder.kt index a986596bb..aac282b9b 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinDeleteBuilder.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinDeleteBuilder.kt @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,20 +15,29 @@ */ package org.mybatis.dynamic.sql.util.kotlin +import org.mybatis.dynamic.sql.SortSpecification import org.mybatis.dynamic.sql.delete.DeleteDSL import org.mybatis.dynamic.sql.delete.DeleteModel import org.mybatis.dynamic.sql.util.Buildable -typealias DeleteCompleter = KotlinDeleteBuilder.() -> Buildable +typealias DeleteCompleter = KotlinDeleteBuilder.() -> Unit class KotlinDeleteBuilder(private val dsl: DeleteDSL) : - KotlinBaseBuilder.DeleteWhereBuilder, KotlinDeleteBuilder>() { + KotlinBaseBuilder>(), Buildable { - fun allRows() = this + fun orderBy(vararg columns: SortSpecification) { + dsl.orderBy(columns.toList()) + } - override fun build(): DeleteModel = dsl.build() + fun limit(limit: Long) { + limitWhenPresent(limit) + } + + fun limitWhenPresent(limit: Long?) { + dsl.limitWhenPresent(limit) + } - override fun getWhere(): DeleteDSL.DeleteWhereBuilder = dsl.where() + override fun build(): DeleteModel = dsl.build() - override fun self() = this + override fun getDsl(): DeleteDSL = dsl } diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinGeneralInsertBuilder.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinGeneralInsertBuilder.kt new file mode 100644 index 000000000..413ddcac4 --- /dev/null +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinGeneralInsertBuilder.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util.kotlin + +import org.mybatis.dynamic.sql.SqlColumn +import org.mybatis.dynamic.sql.SqlTable +import org.mybatis.dynamic.sql.insert.GeneralInsertDSL +import org.mybatis.dynamic.sql.insert.GeneralInsertModel +import org.mybatis.dynamic.sql.util.AbstractColumnMapping +import org.mybatis.dynamic.sql.util.Buildable + +typealias GeneralInsertCompleter = @MyBatisDslMarker KotlinGeneralInsertBuilder.() -> Unit + +@MyBatisDslMarker +class KotlinGeneralInsertBuilder(private val table: SqlTable) : Buildable { + + private val columnMappings = mutableListOf() + + fun set(column: SqlColumn) = GeneralInsertColumnSetCompleter(column) { + columnMappings.add(it) + } + + override fun build(): GeneralInsertModel = + with(GeneralInsertDSL.Builder()) { + withTable(table) + withColumnMappings(columnMappings) + build() + }.build() +} diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinInsertBuilder.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinInsertBuilder.kt new file mode 100644 index 000000000..6b2784fed --- /dev/null +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinInsertBuilder.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util.kotlin + +import org.mybatis.dynamic.sql.SqlColumn +import org.mybatis.dynamic.sql.SqlTable +import org.mybatis.dynamic.sql.insert.InsertDSL +import org.mybatis.dynamic.sql.insert.InsertModel +import org.mybatis.dynamic.sql.util.AbstractColumnMapping +import org.mybatis.dynamic.sql.util.Buildable +import org.mybatis.dynamic.sql.util.MappedColumnMapping +import org.mybatis.dynamic.sql.util.MappedColumnWhenPresentMapping + +typealias KotlinInsertCompleter = KotlinInsertBuilder.() -> Unit + +@MyBatisDslMarker +class KotlinInsertBuilder (private val row: T): Buildable> { + private var table: SqlTable? = null + private val columnMappings = mutableListOf() + + fun into(table: SqlTable) { + this.table = table + } + + fun map(column: SqlColumn) = SingleRowInsertColumnMapCompleter(column) { + columnMappings.add(it) + } + + fun withMappedColumn(column: SqlColumn) { + columnMappings.add(MappedColumnMapping.of(column)) + } + + fun withMappedColumnWhenPresent(column: SqlColumn, valueSupplier: () -> Any?) { + columnMappings.add(MappedColumnWhenPresentMapping.of(column, valueSupplier)) + } + + override fun build(): InsertModel { + assertNotNull(table, "ERROR.25") //$NON-NLS-1$ + return with(InsertDSL.Builder()) { + withRow(row) + withTable(table!!) + withColumnMappings(columnMappings) + build() + }.build() + } +} diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinInsertColumnMapCompleters.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinInsertColumnMapCompleters.kt new file mode 100644 index 000000000..076001b9e --- /dev/null +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinInsertColumnMapCompleters.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util.kotlin + +import org.mybatis.dynamic.sql.SqlColumn +import org.mybatis.dynamic.sql.util.AbstractColumnMapping +import org.mybatis.dynamic.sql.util.ConstantMapping +import org.mybatis.dynamic.sql.util.NullMapping +import org.mybatis.dynamic.sql.util.PropertyMapping +import org.mybatis.dynamic.sql.util.PropertyWhenPresentMapping +import org.mybatis.dynamic.sql.util.RowMapping +import org.mybatis.dynamic.sql.util.StringConstantMapping +import org.mybatis.dynamic.sql.util.ValueMapping +import org.mybatis.dynamic.sql.util.ValueOrNullMapping +import org.mybatis.dynamic.sql.util.ValueWhenPresentMapping + +@MyBatisDslMarker +sealed class AbstractInsertColumnMapCompleter( + internal val column: SqlColumn, + internal val mappingConsumer: (AbstractColumnMapping) -> Unit) { + + fun toNull() = mappingConsumer.invoke(NullMapping.of(column)) + + infix fun toConstant(constant: String) = mappingConsumer.invoke(ConstantMapping.of(column, constant)) + + infix fun toStringConstant(constant: String) = mappingConsumer.invoke(StringConstantMapping.of(column, constant)) +} + +class MultiRowInsertColumnMapCompleter( + column: SqlColumn, + mappingConsumer: (AbstractColumnMapping) -> Unit) + : AbstractInsertColumnMapCompleter(column, mappingConsumer) { + + infix fun toProperty(property: String) = mappingConsumer.invoke(PropertyMapping.of(column, property)) + + fun toRow() = mappingConsumer.invoke(RowMapping.of(column)) +} + +class SingleRowInsertColumnMapCompleter( + column: SqlColumn, + mappingConsumer: (AbstractColumnMapping) -> Unit) + : AbstractInsertColumnMapCompleter(column, mappingConsumer) { + + infix fun toProperty(property: String) = mappingConsumer.invoke(PropertyMapping.of(column, property)) + + fun toPropertyWhenPresent(property: String, valueSupplier: () -> T?) = + mappingConsumer.invoke(PropertyWhenPresentMapping.of(column, property, valueSupplier)) + + fun toRow() = mappingConsumer.invoke(RowMapping.of(column)) +} + +class GeneralInsertColumnSetCompleter( + column: SqlColumn, + mappingConsumer: (AbstractColumnMapping) -> Unit) + : AbstractInsertColumnMapCompleter(column, mappingConsumer) { + + infix fun toValue(value: T) = toValue { value } + + infix fun toValue(value: () -> T) = mappingConsumer.invoke(ValueMapping.of(column, value)) + + infix fun toValueOrNull(value: T?) = toValueOrNull { value } + + infix fun toValueOrNull(value: () -> T?) = mappingConsumer.invoke(ValueOrNullMapping.of(column, value)) + + infix fun toValueWhenPresent(value: T?) = toValueWhenPresent { value } + + infix fun toValueWhenPresent(value: () -> T?) = mappingConsumer.invoke(ValueWhenPresentMapping.of(column, value)) +} diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinMultiRowInsertBuilder.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinMultiRowInsertBuilder.kt new file mode 100644 index 000000000..6b3d72d7c --- /dev/null +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinMultiRowInsertBuilder.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util.kotlin + +import org.mybatis.dynamic.sql.SqlColumn +import org.mybatis.dynamic.sql.SqlTable +import org.mybatis.dynamic.sql.insert.MultiRowInsertDSL +import org.mybatis.dynamic.sql.insert.MultiRowInsertModel +import org.mybatis.dynamic.sql.util.AbstractColumnMapping +import org.mybatis.dynamic.sql.util.Buildable +import org.mybatis.dynamic.sql.util.MappedColumnMapping + +typealias KotlinMultiRowInsertCompleter = KotlinMultiRowInsertBuilder.() -> Unit + +@MyBatisDslMarker +class KotlinMultiRowInsertBuilder (private val rows: Collection): Buildable> { + private var table: SqlTable? = null + private val columnMappings = mutableListOf() + + fun into(table: SqlTable) { + this.table = table + } + + fun map(column: SqlColumn) = MultiRowInsertColumnMapCompleter(column) { + columnMappings.add(it) + } + + fun withMappedColumn(column: SqlColumn) { + columnMappings.add(MappedColumnMapping.of(column)) + } + + override fun build(): MultiRowInsertModel { + assertNotNull(table, "ERROR.26") //$NON-NLS-1$ + return with(MultiRowInsertDSL.Builder()) { + withRecords(rows) + withTable(table!!) + withColumnMappings(columnMappings) + build() + }.build() + } +} diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinMultiSelectBuilder.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinMultiSelectBuilder.kt new file mode 100644 index 000000000..618909cd2 --- /dev/null +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinMultiSelectBuilder.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util.kotlin + +import org.mybatis.dynamic.sql.BasicColumn +import org.mybatis.dynamic.sql.SortSpecification +import org.mybatis.dynamic.sql.SqlBuilder +import org.mybatis.dynamic.sql.configuration.StatementConfiguration +import org.mybatis.dynamic.sql.select.MultiSelectDSL +import org.mybatis.dynamic.sql.select.MultiSelectModel +import org.mybatis.dynamic.sql.util.Buildable + +typealias MultiSelectCompleter = KotlinMultiSelectBuilder.() -> Unit + +@MyBatisDslMarker +class KotlinMultiSelectBuilder: Buildable, KotlinPagingDSL { + private var dsl: MultiSelectDSL? = null + set(value) { + assertNull(field, "ERROR.33") //$NON-NLS-1$ + field = value + } + + fun select(vararg selectList: BasicColumn, completer: SelectCompleter) = + select(selectList.asList(), completer) + + fun select(selectList: List, completer: SelectCompleter) { + val b = KotlinSelectBuilder(SqlBuilder.select(selectList)).apply(completer) + dsl = SqlBuilder.multiSelect(b) + } + + fun selectDistinct(vararg selectList: BasicColumn, completer: SelectCompleter) = + selectDistinct(selectList.asList(), completer) + + fun selectDistinct(selectList: List, completer: SelectCompleter) { + val b = KotlinSelectBuilder(SqlBuilder.selectDistinct(selectList)).apply(completer) + dsl = SqlBuilder.multiSelect(b) + } + + fun union(completer: KotlinSubQueryBuilder.() -> Unit) { + val b = KotlinSubQueryBuilder().apply(completer) + getDsl().union(b) + } + + fun unionAll(completer: KotlinSubQueryBuilder.() -> Unit) { + val b = KotlinSubQueryBuilder().apply(completer) + getDsl().unionAll(b) + } + + fun orderBy(vararg columns: SortSpecification) { + getDsl().orderBy(columns.asList()) + } + + override fun limitWhenPresent(limit: Long?) { + getDsl().limitWhenPresent(limit) + } + + override fun offsetWhenPresent(offset: Long?) { + getDsl().offsetWhenPresent(offset) + } + + override fun fetchFirstWhenPresent(fetchFirstRows: Long?) { + getDsl().fetchFirstWhenPresent(fetchFirstRows).rowsOnly() + } + + fun configureStatement(c: StatementConfiguration.() -> Unit) { + getDsl().configureStatement(c) + } + + override fun build(): MultiSelectModel = + getDsl().build() + + private fun getDsl(): MultiSelectDSL = invalidIfNull(dsl, "ERROR.34") //$NON-NLS-1$ +} diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinPagingDSL.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinPagingDSL.kt new file mode 100644 index 000000000..03dd8082f --- /dev/null +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinPagingDSL.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util.kotlin + +interface KotlinPagingDSL { + fun limit(limit: Long) { + limitWhenPresent(limit) + } + + fun limitWhenPresent(limit: Long?) + + fun offset(offset: Long) { + offsetWhenPresent(offset) + } + + fun offsetWhenPresent(offset: Long?) + + fun fetchFirst(fetchFirstRows: Long) { + fetchFirstWhenPresent(fetchFirstRows) + } + + fun fetchFirstWhenPresent(fetchFirstRows: Long?) +} diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinQueryBuilder.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinQueryBuilder.kt deleted file mode 100644 index 60976a20d..000000000 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinQueryBuilder.kt +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright 2016-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.util.kotlin - -import org.mybatis.dynamic.sql.* -import org.mybatis.dynamic.sql.select.QueryExpressionDSL -import org.mybatis.dynamic.sql.select.SelectDSL -import org.mybatis.dynamic.sql.select.SelectModel -import org.mybatis.dynamic.sql.util.Buildable - -typealias SelectCompleter = KotlinQueryBuilder.() -> Buildable - -class KotlinQueryBuilder(private val dsl: QueryExpressionDSL) : - KotlinBaseJoiningBuilder, QueryExpressionDSL.QueryExpressionWhereBuilder, KotlinQueryBuilder>(dsl) { - - fun groupBy(vararg columns: BasicColumn) = - apply { - dsl.groupBy(*columns) - } - - fun orderBy(vararg columns: SortSpecification) = - apply { - dsl.orderBy(*columns) - } - - fun limit(limit: Long) = - apply { - dsl.limit(limit) - } - - fun offset(offset: Long) = - apply { - dsl.offset(offset) - } - - fun fetchFirst(fetchFirstRows: Long): SelectDSL.FetchFirstFinisher = dsl.fetchFirst(fetchFirstRows) - - fun allRows() = this - - override fun build(): SelectModel = dsl.build() - - override fun getWhere(): QueryExpressionDSL.QueryExpressionWhereBuilder = dsl.where() - - override fun self() = this -} diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinSelectBuilder.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinSelectBuilder.kt new file mode 100644 index 000000000..4c80f9d8c --- /dev/null +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinSelectBuilder.kt @@ -0,0 +1,138 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util.kotlin + +import org.mybatis.dynamic.sql.BasicColumn +import org.mybatis.dynamic.sql.CriteriaGroup +import org.mybatis.dynamic.sql.SortSpecification +import org.mybatis.dynamic.sql.SqlTable +import org.mybatis.dynamic.sql.select.QueryExpressionDSL +import org.mybatis.dynamic.sql.select.SelectModel +import org.mybatis.dynamic.sql.select.SubQuery +import org.mybatis.dynamic.sql.util.Buildable + +typealias SelectCompleter = KotlinSelectBuilder.() -> Unit + +@Suppress("TooManyFunctions") +class KotlinSelectBuilder(private val fromGatherer: QueryExpressionDSL.FromGatherer) : + KotlinBaseJoiningBuilder>(), Buildable, KotlinPagingDSL { + + private var dsl: KQueryExpressionDSL? = null + + fun from(table: SqlTable) { + dsl = KQueryExpressionDSL(fromGatherer, table) + } + + fun from(table: SqlTable, alias: String) { + dsl = KQueryExpressionDSL(fromGatherer, table, alias) + } + + fun from(subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit) { + val builder = KotlinQualifiedSubQueryBuilder().apply(subQuery) + dsl = KQueryExpressionDSL(fromGatherer, builder) + } + + fun groupBy(vararg columns: BasicColumn) { + getDsl().groupBy(columns.toList()) + } + + fun having(criteria: GroupingCriteriaReceiver): Unit = + GroupingCriteriaCollector().apply(criteria).let { + getDsl().applyHaving(it) + } + + fun orderBy(vararg columns: SortSpecification) { + getDsl().orderBy(columns.toList()) + } + + override fun limitWhenPresent(limit: Long?) { + getDsl().limitWhenPresent(limit) + } + + override fun offsetWhenPresent(offset: Long?) { + getDsl().offsetWhenPresent(offset) + } + + override fun fetchFirstWhenPresent(fetchFirstRows: Long?) { + getDsl().fetchFirstWhenPresent(fetchFirstRows).rowsOnly() + } + + fun union(union: KotlinUnionBuilder.() -> Unit): Unit = + union(KotlinUnionBuilder(getDsl().union())) + + fun unionAll(unionAll: KotlinUnionBuilder.() -> Unit): Unit = + unionAll(KotlinUnionBuilder(getDsl().unionAll())) + + fun forUpdate() { + getDsl().forUpdate() + } + + fun forNoKeyUpdate() { + getDsl().forNoKeyUpdate() + } + + fun forShare() { + getDsl().forShare() + } + + fun forKeyShare() { + getDsl().forKeyShare() + } + + fun skipLocked() { + getDsl().skipLocked() + } + + fun nowait() { + getDsl().nowait() + } + + override fun build(): SelectModel = getDsl().build() + + override fun getDsl(): KQueryExpressionDSL = invalidIfNull(dsl, "ERROR.27") //$NON-NLS-1$ +} + +/** + * Extension of the QueryExpressionDSL class that provides access to protected methods in that class. + * We do this especially for having support because we don't want to publicly expose a "having" method + * directly in QueryExpressionDSL as it would be in an odd place for the Java DSL. + */ +class KQueryExpressionDSL: QueryExpressionDSL { + constructor(fromGatherer: FromGatherer, table: SqlTable) : super(fromGatherer, table) + + constructor(fromGatherer: FromGatherer, table: SqlTable, alias: String) : + super(fromGatherer, table, alias) + + constructor(fromGatherer: FromGatherer, subQuery: KotlinQualifiedSubQueryBuilder) : + super(fromGatherer, buildSubQuery(subQuery)) + + internal fun applyHaving(collector: GroupingCriteriaCollector) { + val cg = CriteriaGroup.Builder() + .withInitialCriterion(collector.initialCriterion) + .withSubCriteria(collector.subCriteria) + .build() + applyHaving(cg) + } + + companion object { + fun buildSubQuery(subQuery: KotlinQualifiedSubQueryBuilder): SubQuery = + with(SubQuery.Builder()) { + withSelectModel(subQuery.build()) + withAlias(subQuery.correlationName) + build() + } + } +} diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinSubQueryBuilders.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinSubQueryBuilders.kt new file mode 100644 index 000000000..9439e4058 --- /dev/null +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinSubQueryBuilders.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util.kotlin + +import org.mybatis.dynamic.sql.BasicColumn +import org.mybatis.dynamic.sql.SqlBuilder +import org.mybatis.dynamic.sql.SqlColumn +import org.mybatis.dynamic.sql.SqlTable +import org.mybatis.dynamic.sql.configuration.StatementConfiguration +import org.mybatis.dynamic.sql.insert.InsertSelectModel +import org.mybatis.dynamic.sql.select.SelectModel +import org.mybatis.dynamic.sql.util.Buildable + +@MyBatisDslMarker +sealed class KotlinBaseSubQueryBuilder { + private var selectBuilder: KotlinSelectBuilder? = null + + fun select(vararg selectList: BasicColumn, completer: SelectCompleter): Unit = + select(selectList.toList(), completer) + + fun select(selectList: List, completer: SelectCompleter) { + selectBuilder = KotlinSelectBuilder(SqlBuilder.select(selectList)).apply(completer) + } + + fun selectDistinct(vararg selectList: BasicColumn, completer: SelectCompleter): Unit = + selectDistinct(selectList.toList(), completer) + + fun selectDistinct(selectList: List, completer: SelectCompleter) { + selectBuilder = KotlinSelectBuilder(SqlBuilder.selectDistinct(selectList)).apply(completer) + } + + internal fun buildSelectModel(): SelectModel = invalidIfNull(selectBuilder, "ERROR.28").build() //$NON-NLS-1$ +} + +class KotlinSubQueryBuilder : KotlinBaseSubQueryBuilder(), Buildable { + override fun build(): SelectModel = buildSelectModel() +} + +class KotlinQualifiedSubQueryBuilder : KotlinBaseSubQueryBuilder(), Buildable { + var correlationName: String? = null + + operator fun String.unaryPlus() { + correlationName = this + } + + override fun build(): SelectModel = buildSelectModel() +} + +typealias InsertSelectCompleter = KotlinInsertSelectSubQueryBuilder.() -> Unit + +class KotlinInsertSelectSubQueryBuilder : KotlinBaseSubQueryBuilder(), Buildable { + private var columnList: List>? = null + private var table: SqlTable? = null + private var statementConfigurator: (StatementConfiguration.() -> Unit)? = null + + fun into(table: SqlTable) { + this.table = table + } + + fun columns(vararg columnList: SqlColumn<*>): Unit = columns(columnList.asList()) + + fun columns(columnList: List>) { + this.columnList = columnList + } + + fun configureStatement(c: StatementConfiguration.() -> Unit) { + statementConfigurator = c + } + + override fun build(): InsertSelectModel { + assertNotNull(table, "ERROR.29") //$NON-NLS-1$ + + val dsl = if (columnList == null) { + SqlBuilder.insertInto(table!!) + .withSelectStatement { buildSelectModel() } + } else { + SqlBuilder.insertInto(table!!) + .withColumnList(columnList!!) + .withSelectStatement { buildSelectModel() } + } + + statementConfigurator?.let { dsl.configureStatement(it) } + + return dsl.build() + } +} diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinUnionBuilder.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinUnionBuilder.kt new file mode 100644 index 000000000..b403d675f --- /dev/null +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinUnionBuilder.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util.kotlin + +import org.mybatis.dynamic.sql.BasicColumn +import org.mybatis.dynamic.sql.select.QueryExpressionDSL +import org.mybatis.dynamic.sql.select.SelectModel + +@MyBatisDslMarker +class KotlinUnionBuilder(private val unionBuilder: QueryExpressionDSL.UnionBuilder) { + fun select(vararg selectList: BasicColumn, completer: SelectCompleter): Unit = + select(selectList.toList(), completer) + + fun select(selectList: List, completer: SelectCompleter): Unit = + completer(KotlinSelectBuilder(unionBuilder.select(selectList))) + + fun selectDistinct(vararg selectList: BasicColumn, completer: SelectCompleter): Unit = + selectDistinct(selectList.toList(), completer) + + fun selectDistinct(selectList: List, completer: SelectCompleter): Unit = + completer(KotlinSelectBuilder(unionBuilder.selectDistinct(selectList))) +} diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinUpdateBuilder.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinUpdateBuilder.kt index 8a10346d9..f83e45add 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinUpdateBuilder.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinUpdateBuilder.kt @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,28 +15,87 @@ */ package org.mybatis.dynamic.sql.util.kotlin +import org.mybatis.dynamic.sql.BasicColumn +import org.mybatis.dynamic.sql.SortSpecification import org.mybatis.dynamic.sql.SqlColumn -import org.mybatis.dynamic.sql.insert.InsertDSL -import org.mybatis.dynamic.sql.insert.MultiRowInsertDSL import org.mybatis.dynamic.sql.update.UpdateDSL import org.mybatis.dynamic.sql.update.UpdateModel import org.mybatis.dynamic.sql.util.Buildable -// insert completers are here because sonar doesn't see them as covered if they are in a file by themselves -typealias InsertCompleter = InsertDSL.() -> InsertDSL +typealias UpdateCompleter = KotlinUpdateBuilder.() -> Unit -typealias MultiRowInsertCompleter = MultiRowInsertDSL.() -> MultiRowInsertDSL +class KotlinUpdateBuilder(private val dsl: UpdateDSL) : + KotlinBaseBuilder>(), Buildable { -typealias UpdateCompleter = KotlinUpdateBuilder.() -> Buildable + fun set(column: SqlColumn): KotlinSetClauseFinisher = KotlinSetClauseFinisher(column) -class KotlinUpdateBuilder(private val dsl: UpdateDSL) : - KotlinBaseBuilder.UpdateWhereBuilder, KotlinUpdateBuilder>() { + fun orderBy(vararg columns: SortSpecification) { + dsl.orderBy(columns.toList()) + } - fun set(column: SqlColumn): UpdateDSL.SetClauseFinisher = dsl.set(column) + fun limit(limit: Long) { + limitWhenPresent(limit) + } + + fun limitWhenPresent(limit: Long?) { + dsl.limitWhenPresent(limit) + } override fun build(): UpdateModel = dsl.build() - override fun getWhere(): UpdateDSL.UpdateWhereBuilder = dsl.where() + override fun getDsl(): UpdateDSL = dsl + + @MyBatisDslMarker + @Suppress("TooManyFunctions") + inner class KotlinSetClauseFinisher(private val column: SqlColumn) { + fun equalToNull(): Unit = + applyToDsl { + set(column).equalToNull() + } + + infix fun equalToConstant(constant: String): Unit = + applyToDsl { + set(column).equalToConstant(constant) + } + + infix fun equalToStringConstant(constant: String): Unit = + applyToDsl { + set(column).equalToStringConstant(constant) + } + + infix fun equalTo(value: T): Unit = equalTo { value } + + infix fun equalTo(value: () -> T): Unit = + applyToDsl { + set(column).equalTo(value) + } + + infix fun equalTo(rightColumn: BasicColumn): Unit = + applyToDsl { + set(column).equalTo(rightColumn) + } + + infix fun equalToOrNull(value: T?): Unit = equalToOrNull { value } + + infix fun equalToOrNull(value: () -> T?): Unit = + applyToDsl { + set(column).equalToOrNull(value) + } + + infix fun equalToQueryResult(subQuery: KotlinSubQueryBuilder.() -> Unit): Unit = + applyToDsl { + set(column).equalTo(KotlinSubQueryBuilder().apply(subQuery)) + } + + infix fun equalToWhenPresent(value: () -> T?): Unit = + applyToDsl { + set(column).equalToWhenPresent(value) + } + + infix fun equalToWhenPresent(value: T?): Unit = equalToWhenPresent { value } - override fun self() = this + private fun applyToDsl(block: UpdateDSL.() -> Unit) { + this@KotlinUpdateBuilder.dsl.apply(block) + } + } } diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/CaseDSLs.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/CaseDSLs.kt new file mode 100644 index 000000000..ce33f27b7 --- /dev/null +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/CaseDSLs.kt @@ -0,0 +1,133 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util.kotlin.elements + +import org.mybatis.dynamic.sql.BasicColumn +import org.mybatis.dynamic.sql.RenderableCondition +import org.mybatis.dynamic.sql.select.caseexpression.BasicWhenCondition +import org.mybatis.dynamic.sql.select.caseexpression.ConditionBasedWhenCondition +import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseWhenCondition +import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseWhenCondition +import org.mybatis.dynamic.sql.util.kotlin.GroupingCriteriaCollector +import org.mybatis.dynamic.sql.util.kotlin.assertNotNull +import org.mybatis.dynamic.sql.util.kotlin.assertNull + +class KSearchedCaseDSL : KElseDSL { + internal var elseValue: BasicColumn? = null + private set(value) { + assertNull(field, "ERROR.42") //$NON-NLS-1$ + field = value + } + internal val whenConditions = mutableListOf() + + fun `when`(dslCompleter: SearchedCaseCriteriaCollector.() -> Unit) = + SearchedCaseCriteriaCollector().apply(dslCompleter).run { + assertNotNull(thenValue, "ERROR.47") //$NON-NLS-1$ + whenConditions.add(SearchedCaseWhenCondition.Builder().withInitialCriterion(initialCriterion) + .withSubCriteria(subCriteria) + .withThenValue(thenValue!!) + .build()) + } + + override infix fun `else`(column: BasicColumn) { + this.elseValue = column + } +} + +class SearchedCaseCriteriaCollector : GroupingCriteriaCollector(), KThenDSL { + internal var thenValue: BasicColumn? = null + private set(value) { + assertNull(field, "ERROR.41") //$NON-NLS-1$ + field = value + } + + override infix fun then(column: BasicColumn) { + thenValue = column + } +} + +class KSimpleCaseDSL : KElseDSL { + internal var elseValue: BasicColumn? = null + private set(value) { + assertNull(field, "ERROR.42") //$NON-NLS-1$ + field = value + } + internal val whenConditions = mutableListOf>() + + fun `when`(vararg conditions: RenderableCondition) = + SimpleCaseThenGatherer { whenConditions.add(ConditionBasedWhenCondition(conditions.asList(), it)) } + + fun `when`(vararg values: T) = + SimpleCaseThenGatherer { whenConditions.add(BasicWhenCondition(values.asList(), it)) } + + override infix fun `else`(column: BasicColumn) { + this.elseValue = column + } +} + +class SimpleCaseThenGatherer(private val consumer: (BasicColumn) -> Unit): KThenDSL { + override infix fun then(column: BasicColumn) { + consumer.invoke(column) + } +} + +interface KThenDSL { + infix fun then(value: String) { + then(stringConstant(value)) + } + + infix fun then(value: Boolean) { + then(constant(value.toString())) + } + + infix fun then(value: Int) { + then(constant(value.toString())) + } + + infix fun then(value: Long) { + then(constant(value.toString())) + } + + infix fun then(value: Double) { + then(constant(value.toString())) + } + + infix fun then(column: BasicColumn) +} + +interface KElseDSL { + infix fun `else`(value: String) { + `else`(stringConstant(value)) + } + + infix fun `else`(value: Boolean) { + `else`(constant(value.toString())) + } + + infix fun `else`(value: Int) { + `else`(constant(value.toString())) + } + + infix fun `else`(value: Long) { + `else`(constant(value.toString())) + } + + infix fun `else`(value: Double) { + `else`(constant(value.toString())) + } + + infix fun `else`(column: BasicColumn) +} diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/CastDSL.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/CastDSL.kt new file mode 100644 index 000000000..836c61443 --- /dev/null +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/CastDSL.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util.kotlin.elements + +import org.mybatis.dynamic.sql.BasicColumn +import org.mybatis.dynamic.sql.SqlBuilder +import org.mybatis.dynamic.sql.select.function.Cast +import org.mybatis.dynamic.sql.util.kotlin.assertNull + +class CastDSL { + internal var cast: Cast? = null + private set(value) { + assertNull(field, "ERROR.43") //$NON-NLS-1$ + field = value + } + + infix fun String.`as`(targetType: String) { + cast = SqlBuilder.cast(this).`as`(targetType) + } + + infix fun Double.`as`(targetType: String) { + cast = SqlBuilder.cast(this).`as`(targetType) + } + + infix fun BasicColumn.`as`(targetType: String) { + cast = SqlBuilder.cast(this).`as`(targetType) + } +} diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/ColumnExtensions.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/ColumnExtensions.kt new file mode 100644 index 000000000..c3651d995 --- /dev/null +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/ColumnExtensions.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util.kotlin.elements + +import org.mybatis.dynamic.sql.DerivedColumn +import org.mybatis.dynamic.sql.SqlColumn +import org.mybatis.dynamic.sql.SubQueryColumn +import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseModel +import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseModel + +infix fun DerivedColumn.`as`(alias: String): DerivedColumn = this.`as`(alias) + +infix fun SqlColumn.`as`(alias: String): SqlColumn = this.`as`(alias) + +infix fun SearchedCaseModel.`as`(alias: String): SearchedCaseModel = this.`as`(alias) + +infix fun SimpleCaseModel.`as`(alias: String): SimpleCaseModel = this.`as`(alias) + +infix fun SubQueryColumn.`as`(alias: String): SubQueryColumn = this.`as`(alias) + +/** + * Adds a qualifier to a column for use with table aliases (typically in joins or sub queries). + * This is as close to natural SQL syntax as we can get in Kotlin. Natural SQL would look like + * "qualifier.column". With this function we can say "qualifier(column)". + */ +operator fun String.invoke(column: SqlColumn): SqlColumn = column.qualifiedWith(this) diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/SqlElements.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/SqlElements.kt new file mode 100644 index 000000000..1fe9cfb38 --- /dev/null +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/SqlElements.kt @@ -0,0 +1,406 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +@file:Suppress("TooManyFunctions") +package org.mybatis.dynamic.sql.util.kotlin.elements + +import org.mybatis.dynamic.sql.AndOrCriteriaGroup +import org.mybatis.dynamic.sql.BasicColumn +import org.mybatis.dynamic.sql.BindableColumn +import org.mybatis.dynamic.sql.BoundValue +import org.mybatis.dynamic.sql.Constant +import org.mybatis.dynamic.sql.RenderableCondition +import org.mybatis.dynamic.sql.SortSpecification +import org.mybatis.dynamic.sql.SqlBuilder +import org.mybatis.dynamic.sql.SqlColumn +import org.mybatis.dynamic.sql.StringConstant +import org.mybatis.dynamic.sql.SubQueryColumn +import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseModel +import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseModel +import org.mybatis.dynamic.sql.select.aggregate.Avg +import org.mybatis.dynamic.sql.select.aggregate.Count +import org.mybatis.dynamic.sql.select.aggregate.CountAll +import org.mybatis.dynamic.sql.select.aggregate.CountDistinct +import org.mybatis.dynamic.sql.select.aggregate.Max +import org.mybatis.dynamic.sql.select.aggregate.Min +import org.mybatis.dynamic.sql.select.aggregate.Sum +import org.mybatis.dynamic.sql.select.function.Add +import org.mybatis.dynamic.sql.select.function.Cast +import org.mybatis.dynamic.sql.select.function.Concat +import org.mybatis.dynamic.sql.select.function.Concatenate +import org.mybatis.dynamic.sql.select.function.Divide +import org.mybatis.dynamic.sql.select.function.Lower +import org.mybatis.dynamic.sql.select.function.Multiply +import org.mybatis.dynamic.sql.select.function.OperatorFunction +import org.mybatis.dynamic.sql.select.function.Substring +import org.mybatis.dynamic.sql.select.function.Subtract +import org.mybatis.dynamic.sql.select.function.Upper +import org.mybatis.dynamic.sql.util.kotlin.GroupingCriteriaCollector +import org.mybatis.dynamic.sql.util.kotlin.GroupingCriteriaReceiver +import org.mybatis.dynamic.sql.util.kotlin.KotlinSubQueryBuilder +import org.mybatis.dynamic.sql.util.kotlin.invalidIfNull +import org.mybatis.dynamic.sql.where.condition.IsBetween +import org.mybatis.dynamic.sql.where.condition.IsBetweenWhenPresent +import org.mybatis.dynamic.sql.where.condition.IsEqualTo +import org.mybatis.dynamic.sql.where.condition.IsEqualToColumn +import org.mybatis.dynamic.sql.where.condition.IsEqualToWhenPresent +import org.mybatis.dynamic.sql.where.condition.IsEqualToWithSubselect +import org.mybatis.dynamic.sql.where.condition.IsGreaterThan +import org.mybatis.dynamic.sql.where.condition.IsGreaterThanColumn +import org.mybatis.dynamic.sql.where.condition.IsGreaterThanOrEqualTo +import org.mybatis.dynamic.sql.where.condition.IsGreaterThanOrEqualToColumn +import org.mybatis.dynamic.sql.where.condition.IsGreaterThanOrEqualToWhenPresent +import org.mybatis.dynamic.sql.where.condition.IsGreaterThanOrEqualToWithSubselect +import org.mybatis.dynamic.sql.where.condition.IsGreaterThanWhenPresent +import org.mybatis.dynamic.sql.where.condition.IsGreaterThanWithSubselect +import org.mybatis.dynamic.sql.where.condition.IsIn +import org.mybatis.dynamic.sql.where.condition.IsInCaseInsensitive +import org.mybatis.dynamic.sql.where.condition.IsInCaseInsensitiveWhenPresent +import org.mybatis.dynamic.sql.where.condition.IsInWhenPresent +import org.mybatis.dynamic.sql.where.condition.IsInWithSubselect +import org.mybatis.dynamic.sql.where.condition.IsLessThan +import org.mybatis.dynamic.sql.where.condition.IsLessThanColumn +import org.mybatis.dynamic.sql.where.condition.IsLessThanOrEqualTo +import org.mybatis.dynamic.sql.where.condition.IsLessThanOrEqualToColumn +import org.mybatis.dynamic.sql.where.condition.IsLessThanOrEqualToWhenPresent +import org.mybatis.dynamic.sql.where.condition.IsLessThanOrEqualToWithSubselect +import org.mybatis.dynamic.sql.where.condition.IsLessThanWhenPresent +import org.mybatis.dynamic.sql.where.condition.IsLessThanWithSubselect +import org.mybatis.dynamic.sql.where.condition.IsLike +import org.mybatis.dynamic.sql.where.condition.IsLikeCaseInsensitive +import org.mybatis.dynamic.sql.where.condition.IsLikeCaseInsensitiveWhenPresent +import org.mybatis.dynamic.sql.where.condition.IsLikeWhenPresent +import org.mybatis.dynamic.sql.where.condition.IsNotBetween +import org.mybatis.dynamic.sql.where.condition.IsNotBetweenWhenPresent +import org.mybatis.dynamic.sql.where.condition.IsNotEqualTo +import org.mybatis.dynamic.sql.where.condition.IsNotEqualToColumn +import org.mybatis.dynamic.sql.where.condition.IsNotEqualToWhenPresent +import org.mybatis.dynamic.sql.where.condition.IsNotEqualToWithSubselect +import org.mybatis.dynamic.sql.where.condition.IsNotIn +import org.mybatis.dynamic.sql.where.condition.IsNotInCaseInsensitive +import org.mybatis.dynamic.sql.where.condition.IsNotInCaseInsensitiveWhenPresent +import org.mybatis.dynamic.sql.where.condition.IsNotInWhenPresent +import org.mybatis.dynamic.sql.where.condition.IsNotInWithSubselect +import org.mybatis.dynamic.sql.where.condition.IsNotLike +import org.mybatis.dynamic.sql.where.condition.IsNotLikeCaseInsensitive +import org.mybatis.dynamic.sql.where.condition.IsNotLikeCaseInsensitiveWhenPresent +import org.mybatis.dynamic.sql.where.condition.IsNotLikeWhenPresent +import org.mybatis.dynamic.sql.where.condition.IsNotNull +import org.mybatis.dynamic.sql.where.condition.IsNull + +// support for criteria without initial conditions +fun and(receiver: GroupingCriteriaReceiver): AndOrCriteriaGroup = + with(GroupingCriteriaCollector().apply(receiver)) { + AndOrCriteriaGroup.Builder().withInitialCriterion(initialCriterion) + .withSubCriteria(subCriteria) + .withConnector("and") + .build() + } + +fun or(receiver: GroupingCriteriaReceiver): AndOrCriteriaGroup = + with(GroupingCriteriaCollector().apply(receiver)) { + AndOrCriteriaGroup.Builder().withInitialCriterion(initialCriterion) + .withSubCriteria(subCriteria) + .withConnector("or") + .build() + } + +// case expressions +fun case(dslCompleter: KSearchedCaseDSL.() -> Unit): SearchedCaseModel = + KSearchedCaseDSL().apply(dslCompleter).run { + SearchedCaseModel.Builder() + .withWhenConditions(whenConditions) + .withElseValue(elseValue) + .build() + } + +fun case(column: BindableColumn, dslCompleter: KSimpleCaseDSL.() -> Unit) : SimpleCaseModel = + KSimpleCaseDSL().apply(dslCompleter).run { + SimpleCaseModel.Builder() + .withColumn(column) + .withWhenConditions(whenConditions) + .withElseValue(elseValue) + .build() + } + +// aggregate support +fun count(): CountAll = SqlBuilder.count() + +fun count(column: BasicColumn): Count = SqlBuilder.count(column) + +fun countDistinct(column: BasicColumn): CountDistinct = SqlBuilder.countDistinct(column) + +fun subQuery(subQuery: KotlinSubQueryBuilder.() -> Unit): SubQueryColumn = + SubQueryColumn.of(KotlinSubQueryBuilder().apply(subQuery).build()) + +fun max(column: BindableColumn): Max = SqlBuilder.max(column) + +fun min(column: BindableColumn): Min = SqlBuilder.min(column) + +fun avg(column: BindableColumn): Avg = SqlBuilder.avg(column) + +fun sum(column: BindableColumn): Sum = SqlBuilder.sum(column) + +fun sum(column: BasicColumn): Sum<*> = SqlBuilder.sum(column) + +fun sum(column: BindableColumn, condition: RenderableCondition): Sum = SqlBuilder.sum(column, condition) + +// constants +fun constant(constant: String): Constant = SqlBuilder.constant(constant) + +fun stringConstant(constant: String): StringConstant = SqlBuilder.stringConstant(constant) + +fun value(value: T): BoundValue = SqlBuilder.value(value) + +// functions +fun add( + firstColumn: BindableColumn, + secondColumn: BasicColumn, + vararg subsequentColumns: BasicColumn +): Add = Add.of(firstColumn, secondColumn, subsequentColumns.asList()) + +fun divide( + firstColumn: BindableColumn, + secondColumn: BasicColumn, + vararg subsequentColumns: BasicColumn +): Divide = Divide.of(firstColumn, secondColumn, subsequentColumns.asList()) + +fun multiply( + firstColumn: BindableColumn, + secondColumn: BasicColumn, + vararg subsequentColumns: BasicColumn +): Multiply = Multiply.of(firstColumn, secondColumn, subsequentColumns.asList()) + +fun subtract( + firstColumn: BindableColumn, + secondColumn: BasicColumn, + vararg subsequentColumns: BasicColumn +): Subtract = Subtract.of(firstColumn, secondColumn, subsequentColumns.asList()) + +fun cast(receiver: CastDSL.() -> Unit): Cast = + invalidIfNull(CastDSL().apply(receiver).cast, "ERROR.43") + +fun concat( + firstColumn: BindableColumn, + vararg subsequentColumns: BasicColumn +): Concat = Concat.of(firstColumn, subsequentColumns.asList()) + +fun concatenate( + firstColumn: BindableColumn, + secondColumn: BasicColumn, + vararg subsequentColumns: BasicColumn +): Concatenate = Concatenate.of(firstColumn, secondColumn, subsequentColumns.asList()) + +fun applyOperator( + operator: String, + firstColumn: BindableColumn, + secondColumn: BasicColumn, + vararg subsequentColumns: BasicColumn +): OperatorFunction = OperatorFunction.of(operator, firstColumn, secondColumn, subsequentColumns.asList()) + +fun lower(column: BindableColumn): Lower = SqlBuilder.lower(column) + +fun substring( + column: BindableColumn, + offset: Int, + length: Int +): Substring = SqlBuilder.substring(column, offset, length) + +fun upper(column: BindableColumn): Upper = SqlBuilder.upper(column) + +// conditions for all data types +fun isNull(): IsNull = SqlBuilder.isNull() + +fun isNotNull(): IsNotNull = SqlBuilder.isNotNull() + +fun isEqualTo(value: T): IsEqualTo = SqlBuilder.isEqualTo(value) + +fun isEqualTo(subQuery: KotlinSubQueryBuilder.() -> Unit): IsEqualToWithSubselect = + SqlBuilder.isEqualTo(KotlinSubQueryBuilder().apply(subQuery)) + +fun isEqualTo(column: BasicColumn): IsEqualToColumn = SqlBuilder.isEqualTo(column) + +fun isEqualToWhenPresent(value: T?): IsEqualToWhenPresent = SqlBuilder.isEqualToWhenPresent(value) + +fun isNotEqualTo(value: T): IsNotEqualTo = SqlBuilder.isNotEqualTo(value) + +fun isNotEqualTo(subQuery: KotlinSubQueryBuilder.() -> Unit): IsNotEqualToWithSubselect = + SqlBuilder.isNotEqualTo(KotlinSubQueryBuilder().apply(subQuery)) + +fun isNotEqualTo(column: BasicColumn): IsNotEqualToColumn = SqlBuilder.isNotEqualTo(column) + +fun isNotEqualToWhenPresent(value: T?): IsNotEqualToWhenPresent = + SqlBuilder.isNotEqualToWhenPresent(value) + +fun isGreaterThan(value: T): IsGreaterThan = SqlBuilder.isGreaterThan(value) + +fun isGreaterThan(subQuery: KotlinSubQueryBuilder.() -> Unit): IsGreaterThanWithSubselect = + SqlBuilder.isGreaterThan(KotlinSubQueryBuilder().apply(subQuery)) + +fun isGreaterThan(column: BasicColumn): IsGreaterThanColumn = SqlBuilder.isGreaterThan(column) + +fun isGreaterThanWhenPresent(value: T?): IsGreaterThanWhenPresent = + SqlBuilder.isGreaterThanWhenPresent(value) + +fun isGreaterThanOrEqualTo(value: T): IsGreaterThanOrEqualTo = SqlBuilder.isGreaterThanOrEqualTo(value) + +fun isGreaterThanOrEqualTo(subQuery: KotlinSubQueryBuilder.() -> Unit): IsGreaterThanOrEqualToWithSubselect = + SqlBuilder.isGreaterThanOrEqualTo(KotlinSubQueryBuilder().apply(subQuery)) + +fun isGreaterThanOrEqualTo(column: BasicColumn): IsGreaterThanOrEqualToColumn = + SqlBuilder.isGreaterThanOrEqualTo(column) + +fun isGreaterThanOrEqualToWhenPresent(value: T?): IsGreaterThanOrEqualToWhenPresent = + SqlBuilder.isGreaterThanOrEqualToWhenPresent(value) + +fun isLessThan(value: T): IsLessThan = SqlBuilder.isLessThan(value) + +fun isLessThan(subQuery: KotlinSubQueryBuilder.() -> Unit): IsLessThanWithSubselect = + SqlBuilder.isLessThan(KotlinSubQueryBuilder().apply(subQuery)) + +fun isLessThan(column: BasicColumn): IsLessThanColumn = SqlBuilder.isLessThan(column) + +fun isLessThanWhenPresent(value: T?): IsLessThanWhenPresent = SqlBuilder.isLessThanWhenPresent(value) + +fun isLessThanOrEqualTo(value: T): IsLessThanOrEqualTo = SqlBuilder.isLessThanOrEqualTo(value) + +fun isLessThanOrEqualTo(subQuery: KotlinSubQueryBuilder.() -> Unit): IsLessThanOrEqualToWithSubselect = + SqlBuilder.isLessThanOrEqualTo(KotlinSubQueryBuilder().apply(subQuery)) + +fun isLessThanOrEqualTo(column: BasicColumn): IsLessThanOrEqualToColumn = SqlBuilder.isLessThanOrEqualTo(column) + +fun isLessThanOrEqualToWhenPresent(value: T?): IsLessThanOrEqualToWhenPresent = + SqlBuilder.isLessThanOrEqualToWhenPresent(value) + +fun isIn(vararg values: T): IsIn = isIn(values.asList()) + +fun isIn(values: Collection): IsIn = SqlBuilder.isIn(values) + +fun isIn(subQuery: KotlinSubQueryBuilder.() -> Unit): IsInWithSubselect = + SqlBuilder.isIn(KotlinSubQueryBuilder().apply(subQuery)) + +fun isInWhenPresent(vararg values: T?): IsInWhenPresent = isInWhenPresent(values.asList()) + +fun isInWhenPresent(values: Collection?): IsInWhenPresent = SqlBuilder.isInWhenPresent(values) + +fun isNotIn(vararg values: T): IsNotIn = isNotIn(values.asList()) + +fun isNotIn(values: Collection): IsNotIn = SqlBuilder.isNotIn(values) + +fun isNotIn(subQuery: KotlinSubQueryBuilder.() -> Unit): IsNotInWithSubselect = + SqlBuilder.isNotIn(KotlinSubQueryBuilder().apply(subQuery)) + +fun isNotInWhenPresent(vararg values: T?): IsNotInWhenPresent = isNotInWhenPresent(values.asList()) + +fun isNotInWhenPresent(values: Collection?): IsNotInWhenPresent = SqlBuilder.isNotInWhenPresent(values) + +fun isBetween(value1: T): BetweenBuilder = BetweenBuilder(value1) + +fun isBetweenWhenPresent(value1: T?): BetweenWhenPresentBuilder = BetweenWhenPresentBuilder(value1) + +fun isNotBetween(value1: T): NotBetweenBuilder = NotBetweenBuilder(value1) + +fun isNotBetweenWhenPresent(value1: T?): NotBetweenWhenPresentBuilder = + NotBetweenWhenPresentBuilder(value1) + +// for string columns, but generic for columns with type handlers +fun isLike(value: T): IsLike = SqlBuilder.isLike(value) + +fun isLikeWhenPresent(value: T?): IsLikeWhenPresent = SqlBuilder.isLikeWhenPresent(value) + +fun isNotLike(value: T): IsNotLike = SqlBuilder.isNotLike(value) + +fun isNotLikeWhenPresent(value: T?): IsNotLikeWhenPresent = SqlBuilder.isNotLikeWhenPresent(value) + +// shortcuts for booleans +fun isTrue(): IsEqualTo = isEqualTo(true) + +fun isFalse(): IsEqualTo = isEqualTo(false) + +// conditions for strings only +fun isLikeCaseInsensitive(value: String): IsLikeCaseInsensitive = SqlBuilder.isLikeCaseInsensitive(value) + +fun isLikeCaseInsensitiveWhenPresent(value: String?): IsLikeCaseInsensitiveWhenPresent = + SqlBuilder.isLikeCaseInsensitiveWhenPresent(value) + +fun isNotLikeCaseInsensitive(value: String): IsNotLikeCaseInsensitive = SqlBuilder.isNotLikeCaseInsensitive(value) + +fun isNotLikeCaseInsensitiveWhenPresent(value: String?): IsNotLikeCaseInsensitiveWhenPresent = + SqlBuilder.isNotLikeCaseInsensitiveWhenPresent(value) + +fun isInCaseInsensitive(vararg values: String): IsInCaseInsensitive = isInCaseInsensitive(values.asList()) + +fun isInCaseInsensitive(values: Collection): IsInCaseInsensitive = + SqlBuilder.isInCaseInsensitive(values) + +fun isInCaseInsensitiveWhenPresent(vararg values: String?): IsInCaseInsensitiveWhenPresent = + isInCaseInsensitiveWhenPresent(values.asList()) + +fun isInCaseInsensitiveWhenPresent(values: Collection?): IsInCaseInsensitiveWhenPresent = + SqlBuilder.isInCaseInsensitiveWhenPresent(values) + +fun isNotInCaseInsensitive(vararg values: String): IsNotInCaseInsensitive = + isNotInCaseInsensitive(values.asList()) + +fun isNotInCaseInsensitive(values: Collection): IsNotInCaseInsensitive = + SqlBuilder.isNotInCaseInsensitive(values) + +fun isNotInCaseInsensitiveWhenPresent(vararg values: String?): IsNotInCaseInsensitiveWhenPresent = + isNotInCaseInsensitiveWhenPresent(values.asList()) + +fun isNotInCaseInsensitiveWhenPresent(values: Collection?): IsNotInCaseInsensitiveWhenPresent = + SqlBuilder.isNotInCaseInsensitiveWhenPresent(values) + +// order by support +/** + * Creates a sort specification based on a String. This is useful when a column has been + * aliased in the select list. + * + * @param name the string to use as a sort specification + * @return a sort specification + */ +fun sortColumn(name: String): SortSpecification = SqlBuilder.sortColumn(name) + +/** + * Creates a sort specification based on a column and a table alias. This can be useful in a join + * where the desired sort order is based on a column not in the select list. This will likely + * fail in union queries depending on database support. + * + * @param tableAlias the table alias + * @param column the column + * @return a sort specification + */ +fun sortColumn(tableAlias: String, column: SqlColumn<*>): SortSpecification = SqlBuilder.sortColumn(tableAlias, column) + +// DSL Support Classes +class BetweenBuilder(private val value1: T) { + fun and(value2: T): IsBetween = SqlBuilder.isBetween(value1).and(value2) +} + +class BetweenWhenPresentBuilder(private val value1: T?) { + fun and(value2: T?): IsBetweenWhenPresent { + return SqlBuilder.isBetweenWhenPresent(value1).and(value2) + } +} + +class NotBetweenBuilder(private val value1: T) { + fun and(value2: T): IsNotBetween = SqlBuilder.isNotBetween(value1).and(value2) +} + +class NotBetweenWhenPresentBuilder(private val value1: T?) { + fun and(value2: T?): IsNotBetweenWhenPresent { + return SqlBuilder.isNotBetweenWhenPresent(value1).and(value2) + } +} diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/SqlTableExtensions.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/SqlTableExtensions.kt new file mode 100644 index 000000000..24962ef01 --- /dev/null +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/SqlTableExtensions.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.mybatis.dynamic.sql.util.kotlin.elements + +import org.mybatis.dynamic.sql.SqlColumn +import org.mybatis.dynamic.sql.SqlTable +import org.mybatis.dynamic.sql.render.RenderingStrategy +import java.sql.JDBCType +import kotlin.reflect.KClass + +/** + * This function replaces the native functions in [@see SqlColumn] such as + * [@see SqlColumn#withTypeHandler], [@see SqlColumn#withRenderingStrategy], etc. + * This function preserves the non-nullable column type which is lost with the Java + * native versions. + */ +@SuppressWarnings("LongParameterList") +fun SqlTable.column( + name: String, + jdbcType: JDBCType? = null, + typeHandler: String? = null, + renderingStrategy: RenderingStrategy? = null, + parameterTypeConverter: ((T?) -> Any?) = { it }, + javaType: KClass? = null, + javaProperty: String? = null, +): SqlColumn = SqlColumn.Builder().run { + withTable(this@column) + withName(name) + withJdbcType(jdbcType) + withTypeHandler(typeHandler) + withRenderingStrategy(renderingStrategy) + withParameterTypeConverter(parameterTypeConverter) + withJavaType(javaType?.java) + withJavaProperty(javaProperty) + build() +} diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/model/ModelBuilderFunctions.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/model/ModelBuilderFunctions.kt new file mode 100644 index 000000000..bd25cfab6 --- /dev/null +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/model/ModelBuilderFunctions.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +@file:Suppress("TooManyFunctions") +package org.mybatis.dynamic.sql.util.kotlin.model + +import org.mybatis.dynamic.sql.BasicColumn +import org.mybatis.dynamic.sql.SqlBuilder +import org.mybatis.dynamic.sql.SqlTable +import org.mybatis.dynamic.sql.delete.DeleteModel +import org.mybatis.dynamic.sql.insert.BatchInsertModel +import org.mybatis.dynamic.sql.insert.GeneralInsertModel +import org.mybatis.dynamic.sql.insert.InsertModel +import org.mybatis.dynamic.sql.insert.InsertSelectModel +import org.mybatis.dynamic.sql.insert.MultiRowInsertModel +import org.mybatis.dynamic.sql.select.MultiSelectModel +import org.mybatis.dynamic.sql.select.SelectModel +import org.mybatis.dynamic.sql.update.UpdateModel +import org.mybatis.dynamic.sql.util.kotlin.CountCompleter +import org.mybatis.dynamic.sql.util.kotlin.DeleteCompleter +import org.mybatis.dynamic.sql.util.kotlin.GeneralInsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.InsertSelectCompleter +import org.mybatis.dynamic.sql.util.kotlin.KotlinBatchInsertBuilder +import org.mybatis.dynamic.sql.util.kotlin.KotlinBatchInsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.KotlinCountBuilder +import org.mybatis.dynamic.sql.util.kotlin.KotlinDeleteBuilder +import org.mybatis.dynamic.sql.util.kotlin.KotlinGeneralInsertBuilder +import org.mybatis.dynamic.sql.util.kotlin.KotlinInsertBuilder +import org.mybatis.dynamic.sql.util.kotlin.KotlinInsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.KotlinInsertSelectSubQueryBuilder +import org.mybatis.dynamic.sql.util.kotlin.KotlinMultiRowInsertBuilder +import org.mybatis.dynamic.sql.util.kotlin.KotlinMultiRowInsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.KotlinMultiSelectBuilder +import org.mybatis.dynamic.sql.util.kotlin.KotlinSelectBuilder +import org.mybatis.dynamic.sql.util.kotlin.KotlinUpdateBuilder +import org.mybatis.dynamic.sql.util.kotlin.MultiSelectCompleter +import org.mybatis.dynamic.sql.util.kotlin.SelectCompleter +import org.mybatis.dynamic.sql.util.kotlin.UpdateCompleter + +fun count(column: BasicColumn, completer: CountCompleter): SelectModel = + KotlinCountBuilder(SqlBuilder.countColumn(column)).apply(completer).build() + +fun countDistinct(column: BasicColumn, completer: CountCompleter): SelectModel = + KotlinCountBuilder(SqlBuilder.countDistinctColumn(column)).apply(completer).build() + +fun countFrom(table: SqlTable, completer: CountCompleter): SelectModel = + KotlinCountBuilder(SqlBuilder.countColumn(SqlBuilder.constant("*"))) + .from(table).apply(completer).build() + +fun deleteFrom(table: SqlTable, completer: DeleteCompleter): DeleteModel = + KotlinDeleteBuilder(SqlBuilder.deleteFrom(table)).apply(completer).build() + +fun deleteFrom(table: SqlTable, tableAlias: String, completer: DeleteCompleter): DeleteModel = + KotlinDeleteBuilder(SqlBuilder.deleteFrom(table, tableAlias)).apply(completer).build() + +fun insert(row: T, completer: KotlinInsertCompleter): InsertModel = + KotlinInsertBuilder(row).apply(completer).build() + +fun insertBatch(rows: Collection, completer: KotlinBatchInsertCompleter): BatchInsertModel = + KotlinBatchInsertBuilder(rows).apply(completer).build() + +fun insertInto(table: SqlTable, completer: GeneralInsertCompleter): GeneralInsertModel = + KotlinGeneralInsertBuilder(table).apply(completer).build() + +fun insertMultiple(rows: Collection, completer: KotlinMultiRowInsertCompleter): MultiRowInsertModel = + KotlinMultiRowInsertBuilder(rows).apply(completer).build() + +fun insertSelect(completer: InsertSelectCompleter): InsertSelectModel = + KotlinInsertSelectSubQueryBuilder().apply(completer).build() + +fun select(vararg columns: BasicColumn, completer: SelectCompleter): SelectModel = + select(columns.asList(), completer) + +fun select(columns: List, completer: SelectCompleter): SelectModel = + KotlinSelectBuilder(SqlBuilder.select(columns)).apply(completer).build() + +fun selectDistinct(vararg columns: BasicColumn, completer: SelectCompleter): SelectModel = + selectDistinct(columns.asList(), completer) + +fun selectDistinct(columns: List, completer: SelectCompleter): SelectModel = + KotlinSelectBuilder(SqlBuilder.selectDistinct(columns)).apply(completer).build() + +fun multiSelect(completer: MultiSelectCompleter): MultiSelectModel = + KotlinMultiSelectBuilder().apply(completer).build() + +fun update(table: SqlTable, completer: UpdateCompleter): UpdateModel = + KotlinUpdateBuilder(SqlBuilder.update(table)).apply(completer).build() + +fun update(table: SqlTable, tableAlias: String, completer: UpdateCompleter): UpdateModel = + KotlinUpdateBuilder(SqlBuilder.update(table, tableAlias)).apply(completer).build() diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/mybatis3/MapperSupportFunctions.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/mybatis3/MapperSupportFunctions.kt index 9832234b3..e43ed38ee 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/mybatis3/MapperSupportFunctions.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/mybatis3/MapperSupportFunctions.kt @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -13,46 +13,158 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:Suppress("TooManyFunctions") package org.mybatis.dynamic.sql.util.kotlin.mybatis3 import org.mybatis.dynamic.sql.BasicColumn -import org.mybatis.dynamic.sql.SqlBuilder import org.mybatis.dynamic.sql.SqlTable import org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider +import org.mybatis.dynamic.sql.insert.render.GeneralInsertStatementProvider +import org.mybatis.dynamic.sql.insert.render.InsertSelectStatementProvider import org.mybatis.dynamic.sql.insert.render.InsertStatementProvider import org.mybatis.dynamic.sql.insert.render.MultiRowInsertStatementProvider -import org.mybatis.dynamic.sql.select.QueryExpressionDSL -import org.mybatis.dynamic.sql.select.SelectModel import org.mybatis.dynamic.sql.select.render.SelectStatementProvider import org.mybatis.dynamic.sql.update.render.UpdateStatementProvider -import org.mybatis.dynamic.sql.util.kotlin.* +import org.mybatis.dynamic.sql.util.kotlin.CountCompleter +import org.mybatis.dynamic.sql.util.kotlin.DeleteCompleter +import org.mybatis.dynamic.sql.util.kotlin.GeneralInsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.InsertSelectCompleter +import org.mybatis.dynamic.sql.util.kotlin.KotlinBatchInsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.KotlinInsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.KotlinMultiRowInsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.SelectCompleter +import org.mybatis.dynamic.sql.util.kotlin.UpdateCompleter + +fun count( + mapper: (SelectStatementProvider) -> Long, + column: BasicColumn, + table: SqlTable, + completer: CountCompleter +): Long = + count(column) { + from(table) + run(completer) + }.run(mapper) + +fun countDistinct( + mapper: (SelectStatementProvider) -> Long, + column: BasicColumn, + table: SqlTable, + completer: CountCompleter +): Long = + countDistinct(column) { + from(table) + run(completer) + }.run(mapper) + +fun countFrom(mapper: (SelectStatementProvider) -> Long, table: SqlTable, completer: CountCompleter): Long = + countFrom(table, completer).run(mapper) -fun countFrom(mapper: (SelectStatementProvider) -> Long, table: SqlTable, completer: CountCompleter) = - mapper(countFrom(table, completer)) +fun deleteFrom(mapper: (DeleteStatementProvider) -> Int, table: SqlTable, completer: DeleteCompleter): Int = + deleteFrom(table, completer).run(mapper) -fun deleteFrom(mapper: (DeleteStatementProvider) -> Int, table: SqlTable, completer: DeleteCompleter) = - mapper(deleteFrom(table, completer)) +fun insert( + mapper: (InsertStatementProvider) -> Int, + row: T, + table: SqlTable, + completer: KotlinInsertCompleter +): Int = + insert(row) { + into(table) + run(completer) + }.run(mapper) + +/** + * This function simply inserts all rows using the supplied mapper. It is up + * to the user to manage MyBatis3 batch processing externally. When executed with a SqlSession + * in batch mode, the return value will not contain relevant update counts (each entry in the + * list will be [org.apache.ibatis.executor.BatchExecutor.BATCH_UPDATE_RETURN_VALUE]). + * To retrieve update counts, execute [org.apache.ibatis.session.SqlSession.flushStatements]. + */ +fun insertBatch( + mapper: (InsertStatementProvider) -> Int, + records: Collection, + table: SqlTable, + completer: KotlinBatchInsertCompleter +): List = + insertBatch(records) { + into(table) + run(completer) + }.insertStatements().map(mapper) -fun insert(mapper: (InsertStatementProvider) -> Int, record: T, table: SqlTable, completer: InsertCompleter) = - mapper(SqlBuilder.insert(record).into(table, completer)) +fun insertInto( + mapper: (GeneralInsertStatementProvider) -> Int, + table: SqlTable, + completer: GeneralInsertCompleter +): Int = + insertInto(table, completer).run(mapper) -fun insertMultiple(mapper: (MultiRowInsertStatementProvider) -> Int, records: Collection, table: SqlTable, completer: MultiRowInsertCompleter) = - mapper(SqlBuilder.insertMultiple(records).into(table, completer)) +fun insertMultiple( + mapper: (MultiRowInsertStatementProvider) -> Int, + records: Collection, + table: SqlTable, + completer: KotlinMultiRowInsertCompleter +): Int = + insertMultiple(records) { + into(table) + run(completer) + }.run(mapper) -fun selectDistinct(mapper: (SelectStatementProvider) -> List, selectList: List, table: SqlTable, completer: SelectCompleter) = - mapper(SqlBuilder.selectDistinct(selectList).from(table, completer)) +fun insertMultipleWithGeneratedKeys( + mapper: (String, List) -> Int, + records: Collection, + table: SqlTable, + completer: KotlinMultiRowInsertCompleter +): Int = + insertMultiple(records) { + into(table) + run(completer) + }.run { + mapper(insertStatement, this.records) + } -fun selectList(mapper: (SelectStatementProvider) -> List, selectList: List, table: SqlTable, completer: SelectCompleter) = - mapper(SqlBuilder.select(selectList).from(table, completer)) +fun insertSelect( + mapper: (InsertSelectStatementProvider) -> Int, + table: SqlTable, + completer: InsertSelectCompleter +): Int = + insertSelect { + into(table) + run(completer) + }.run(mapper) -fun selectList(mapper: (SelectStatementProvider) -> List, start: QueryExpressionDSL, completer: SelectCompleter) = - mapper(select(start, completer)) +fun selectDistinct( + mapper: (SelectStatementProvider) -> List, + selectList: List, + table: SqlTable, + completer: SelectCompleter +): List = + selectDistinct(selectList) { + from(table) + run(completer) + }.run(mapper) -fun selectOne(mapper: (SelectStatementProvider) -> T?, selectList: List, table: SqlTable, completer: SelectCompleter) = - mapper(SqlBuilder.select(selectList).from(table, completer)) +fun selectList( + mapper: (SelectStatementProvider) -> List, + selectList: List, + table: SqlTable, + completer: SelectCompleter +): List = + select(selectList) { + from(table) + run(completer) + }.run(mapper) -fun selectOne(mapper: (SelectStatementProvider) -> T?, start: QueryExpressionDSL, completer: SelectCompleter) = - mapper(select(start, completer)) +fun selectOne( + mapper: (SelectStatementProvider) -> T?, + selectList: List, + table: SqlTable, + completer: SelectCompleter +): T? = + select(selectList) { + from(table) + run(completer) + }.run(mapper) -fun update(mapper: (UpdateStatementProvider) -> Int, table: SqlTable, completer: UpdateCompleter) = - mapper(update(table, completer)) +fun update(mapper: (UpdateStatementProvider) -> Int, table: SqlTable, completer: UpdateCompleter): Int = + update(table, completer).run(mapper) diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/mybatis3/ProviderBuilderFunctions.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/mybatis3/ProviderBuilderFunctions.kt index e6bc19aa9..8324597bc 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/mybatis3/ProviderBuilderFunctions.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/mybatis3/ProviderBuilderFunctions.kt @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -13,60 +13,93 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:Suppress("TooManyFunctions") package org.mybatis.dynamic.sql.util.kotlin.mybatis3 -import org.mybatis.dynamic.sql.SqlBuilder +import org.mybatis.dynamic.sql.BasicColumn import org.mybatis.dynamic.sql.SqlTable import org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider -import org.mybatis.dynamic.sql.insert.InsertDSL -import org.mybatis.dynamic.sql.insert.MultiRowInsertDSL +import org.mybatis.dynamic.sql.insert.render.BatchInsert +import org.mybatis.dynamic.sql.insert.render.GeneralInsertStatementProvider +import org.mybatis.dynamic.sql.insert.render.InsertSelectStatementProvider import org.mybatis.dynamic.sql.insert.render.InsertStatementProvider import org.mybatis.dynamic.sql.insert.render.MultiRowInsertStatementProvider import org.mybatis.dynamic.sql.render.RenderingStrategies -import org.mybatis.dynamic.sql.select.QueryExpressionDSL -import org.mybatis.dynamic.sql.select.SelectModel import org.mybatis.dynamic.sql.select.render.SelectStatementProvider import org.mybatis.dynamic.sql.update.render.UpdateStatementProvider -import org.mybatis.dynamic.sql.util.kotlin.* - -fun countFrom(table: SqlTable, completer: CountCompleter): SelectStatementProvider { - val builder = KotlinCountBuilder(SqlBuilder.countFrom(table)) - completer(builder) - return builder.build().render(RenderingStrategies.MYBATIS3) -} - -fun deleteFrom(table: SqlTable, completer: DeleteCompleter): DeleteStatementProvider { - val builder = KotlinDeleteBuilder(SqlBuilder.deleteFrom(table)) - completer(builder) - return builder.build().render(RenderingStrategies.MYBATIS3) -} - -fun InsertDSL.IntoGatherer.into(table: SqlTable, completer: InsertCompleter): InsertStatementProvider = - completer(into(table)).build().render(RenderingStrategies.MYBATIS3) - -fun MultiRowInsertDSL.IntoGatherer.into(table: SqlTable, completer: MultiRowInsertCompleter): MultiRowInsertStatementProvider = - completer(into(table)).build().render(RenderingStrategies.MYBATIS3) - -fun QueryExpressionDSL.FromGatherer.from(table: SqlTable, completer: SelectCompleter): SelectStatementProvider { - val builder = KotlinQueryBuilder(from(table)) - completer(builder) - return builder.build().render(RenderingStrategies.MYBATIS3) -} - -fun QueryExpressionDSL.FromGatherer.from(table: SqlTable, alias: String, completer: SelectCompleter): SelectStatementProvider { - val builder = KotlinQueryBuilder(from(table, alias)) - completer(builder) - return builder.build().render(RenderingStrategies.MYBATIS3) -} - -fun select(start: QueryExpressionDSL, completer: SelectCompleter): SelectStatementProvider { - val builder = KotlinQueryBuilder(start) - completer(builder) - return builder.build().render(RenderingStrategies.MYBATIS3) -} - -fun update(table: SqlTable, completer: UpdateCompleter): UpdateStatementProvider { - val builder = KotlinUpdateBuilder(SqlBuilder.update(table)) - completer(builder) - return builder.build().render(RenderingStrategies.MYBATIS3) -} +import org.mybatis.dynamic.sql.util.kotlin.CountCompleter +import org.mybatis.dynamic.sql.util.kotlin.DeleteCompleter +import org.mybatis.dynamic.sql.util.kotlin.GeneralInsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.InsertSelectCompleter +import org.mybatis.dynamic.sql.util.kotlin.KotlinBatchInsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.KotlinInsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.KotlinMultiRowInsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.MultiSelectCompleter +import org.mybatis.dynamic.sql.util.kotlin.SelectCompleter +import org.mybatis.dynamic.sql.util.kotlin.UpdateCompleter +import org.mybatis.dynamic.sql.util.kotlin.model.count +import org.mybatis.dynamic.sql.util.kotlin.model.countDistinct +import org.mybatis.dynamic.sql.util.kotlin.model.countFrom +import org.mybatis.dynamic.sql.util.kotlin.model.deleteFrom +import org.mybatis.dynamic.sql.util.kotlin.model.insert +import org.mybatis.dynamic.sql.util.kotlin.model.insertBatch +import org.mybatis.dynamic.sql.util.kotlin.model.insertInto +import org.mybatis.dynamic.sql.util.kotlin.model.insertMultiple +import org.mybatis.dynamic.sql.util.kotlin.model.insertSelect +import org.mybatis.dynamic.sql.util.kotlin.model.select +import org.mybatis.dynamic.sql.util.kotlin.model.selectDistinct +import org.mybatis.dynamic.sql.util.kotlin.model.update + +fun count(column: BasicColumn, completer: CountCompleter): SelectStatementProvider = + count(column, completer).render(RenderingStrategies.MYBATIS3) + +fun countDistinct(column: BasicColumn, completer: CountCompleter): SelectStatementProvider = + countDistinct(column, completer).render(RenderingStrategies.MYBATIS3) + +fun countFrom(table: SqlTable, completer: CountCompleter): SelectStatementProvider = + countFrom(table, completer).render(RenderingStrategies.MYBATIS3) + +fun deleteFrom(table: SqlTable, completer: DeleteCompleter): DeleteStatementProvider = + deleteFrom(table, completer).render(RenderingStrategies.MYBATIS3) + +fun deleteFrom(table: SqlTable, tableAlias: String, completer: DeleteCompleter): DeleteStatementProvider = + deleteFrom(table, tableAlias, completer).render(RenderingStrategies.MYBATIS3) + +fun insert(row: T, completer: KotlinInsertCompleter): InsertStatementProvider = + insert(row, completer).render(RenderingStrategies.MYBATIS3) + +fun insertBatch(rows: Collection, completer: KotlinBatchInsertCompleter): BatchInsert = + insertBatch(rows, completer).render(RenderingStrategies.MYBATIS3) + +fun insertInto(table: SqlTable, completer: GeneralInsertCompleter): GeneralInsertStatementProvider = + insertInto(table, completer).render(RenderingStrategies.MYBATIS3) + +fun insertMultiple( + rows: Collection, + completer: KotlinMultiRowInsertCompleter +): MultiRowInsertStatementProvider = + insertMultiple(rows, completer).render(RenderingStrategies.MYBATIS3) + +fun insertSelect(completer: InsertSelectCompleter): InsertSelectStatementProvider = + insertSelect(completer).render(RenderingStrategies.MYBATIS3) + +fun select(vararg columns: BasicColumn, completer: SelectCompleter): SelectStatementProvider = + select(columns.asList(), completer).render(RenderingStrategies.MYBATIS3) + +fun select(columns: List, completer: SelectCompleter): SelectStatementProvider = + select(columns, completer).render(RenderingStrategies.MYBATIS3) + +fun selectDistinct(vararg columns: BasicColumn, completer: SelectCompleter): SelectStatementProvider = + selectDistinct(columns.asList(), completer).render(RenderingStrategies.MYBATIS3) + +fun selectDistinct(columns: List, completer: SelectCompleter): SelectStatementProvider = + selectDistinct(columns, completer).render(RenderingStrategies.MYBATIS3) + +fun multiSelect(completer: MultiSelectCompleter): SelectStatementProvider = + org.mybatis.dynamic.sql.util.kotlin.model.multiSelect(completer).render(RenderingStrategies.MYBATIS3) + +fun update(table: SqlTable, completer: UpdateCompleter): UpdateStatementProvider = + update(table, completer).render(RenderingStrategies.MYBATIS3) + +fun update(table: SqlTable, tableAlias: String, completer: UpdateCompleter): UpdateStatementProvider = + update(table, tableAlias, completer).render(RenderingStrategies.MYBATIS3) diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/spring/NamedParameterJdbcTemplateExtensions.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/spring/NamedParameterJdbcTemplateExtensions.kt index eebd52318..6cfd88151 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/spring/NamedParameterJdbcTemplateExtensions.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/spring/NamedParameterJdbcTemplateExtensions.kt @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -13,90 +13,267 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:Suppress("TooManyFunctions") package org.mybatis.dynamic.sql.util.kotlin.spring import org.mybatis.dynamic.sql.BasicColumn -import org.mybatis.dynamic.sql.SqlBuilder import org.mybatis.dynamic.sql.SqlTable import org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider +import org.mybatis.dynamic.sql.insert.render.BatchInsert +import org.mybatis.dynamic.sql.insert.render.GeneralInsertStatementProvider +import org.mybatis.dynamic.sql.insert.render.InsertSelectStatementProvider import org.mybatis.dynamic.sql.insert.render.InsertStatementProvider +import org.mybatis.dynamic.sql.insert.render.MultiRowInsertStatementProvider import org.mybatis.dynamic.sql.select.render.SelectStatementProvider import org.mybatis.dynamic.sql.update.render.UpdateStatementProvider -import org.mybatis.dynamic.sql.util.kotlin.* +import org.mybatis.dynamic.sql.util.kotlin.CountCompleter +import org.mybatis.dynamic.sql.util.kotlin.DeleteCompleter +import org.mybatis.dynamic.sql.util.kotlin.GeneralInsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.InsertSelectCompleter +import org.mybatis.dynamic.sql.util.kotlin.KotlinBatchInsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.KotlinInsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.KotlinMultiRowInsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.MyBatisDslMarker +import org.mybatis.dynamic.sql.util.kotlin.SelectCompleter +import org.mybatis.dynamic.sql.util.kotlin.UpdateCompleter +import org.mybatis.dynamic.sql.util.spring.BatchInsertUtility +import org.springframework.dao.EmptyResultDataAccessException +import org.springframework.jdbc.core.RowMapper import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate +import org.springframework.jdbc.support.KeyHolder import java.sql.ResultSet +import kotlin.reflect.KClass -fun NamedParameterJdbcTemplate.count(selectStatement: SelectStatementProvider) = +fun NamedParameterJdbcTemplate.count(selectStatement: SelectStatementProvider): Long = queryForObject(selectStatement.selectStatement, selectStatement.parameters, Long::class.java)!! -fun NamedParameterJdbcTemplate.countFrom(table: SqlTable, completer: CountCompleter) = +fun NamedParameterJdbcTemplate.count(column: BasicColumn, completer: CountCompleter): Long = + count(org.mybatis.dynamic.sql.util.kotlin.spring.count(column, completer)) + +fun NamedParameterJdbcTemplate.countDistinct(column: BasicColumn, completer: CountCompleter): Long = + count(org.mybatis.dynamic.sql.util.kotlin.spring.countDistinct(column, completer)) + +fun NamedParameterJdbcTemplate.countFrom(table: SqlTable, completer: CountCompleter): Long = count(org.mybatis.dynamic.sql.util.kotlin.spring.countFrom(table, completer)) -fun NamedParameterJdbcTemplate.delete(deleteStatement: DeleteStatementProvider) = +fun NamedParameterJdbcTemplate.delete(deleteStatement: DeleteStatementProvider): Int = update(deleteStatement.deleteStatement, deleteStatement.parameters) -fun NamedParameterJdbcTemplate.deleteFrom(table: SqlTable, completer: DeleteCompleter) = +fun NamedParameterJdbcTemplate.deleteFrom(table: SqlTable, completer: DeleteCompleter): Int = delete(org.mybatis.dynamic.sql.util.kotlin.spring.deleteFrom(table, completer)) -fun NamedParameterJdbcTemplate.insert(insertStatement: InsertStatementProvider) = - update(insertStatement.insertStatement, BeanPropertySqlParameterSource(insertStatement.record)) +// batch insert +fun NamedParameterJdbcTemplate.insertBatch(insertStatement: BatchInsert): IntArray = + batchUpdate(insertStatement.insertStatementSQL, BatchInsertUtility.createBatch(insertStatement.records)) + +fun NamedParameterJdbcTemplate.insertBatch( + vararg records: T, + completer: KotlinBatchInsertCompleter +): IntArray = + insertBatch(records.asList(), completer) + +fun NamedParameterJdbcTemplate.insertBatch( + records: List, + completer: KotlinBatchInsertCompleter +): IntArray = + insertBatch(org.mybatis.dynamic.sql.util.kotlin.spring.insertBatch(records, completer)) + +// single row insert +fun NamedParameterJdbcTemplate.insert(insertStatement: InsertStatementProvider): Int = + update(insertStatement.insertStatement, BeanPropertySqlParameterSource(insertStatement)) + +fun NamedParameterJdbcTemplate.insert( + insertStatement: InsertStatementProvider, + keyHolder: KeyHolder +): Int = + update(insertStatement.insertStatement, BeanPropertySqlParameterSource(insertStatement), keyHolder) + +fun NamedParameterJdbcTemplate.insert(row: T, completer: KotlinInsertCompleter): Int = + insert(org.mybatis.dynamic.sql.util.kotlin.spring.insert(row, completer)) + +// general insert +fun NamedParameterJdbcTemplate.generalInsert(insertStatement: GeneralInsertStatementProvider): Int = + update(insertStatement.insertStatement, insertStatement.parameters) + +fun NamedParameterJdbcTemplate.generalInsert( + insertStatement: GeneralInsertStatementProvider, + keyHolder: KeyHolder +): Int = + update(insertStatement.insertStatement, MapSqlParameterSource(insertStatement.parameters), keyHolder) + +fun NamedParameterJdbcTemplate.insertInto(table: SqlTable, completer: GeneralInsertCompleter): Int = + generalInsert(org.mybatis.dynamic.sql.util.kotlin.spring.insertInto(table, completer)) + +// multiple row insert +fun NamedParameterJdbcTemplate.insertMultiple( + vararg records: T, + completer: KotlinMultiRowInsertCompleter +): Int = + insertMultiple(records.asList(), completer) + +fun NamedParameterJdbcTemplate.insertMultiple( + records: List, + completer: KotlinMultiRowInsertCompleter +): Int = + insertMultiple(org.mybatis.dynamic.sql.util.kotlin.spring.insertMultiple(records, completer)) + +fun NamedParameterJdbcTemplate.insertMultiple(insertStatement: MultiRowInsertStatementProvider): Int = + update(insertStatement.insertStatement, BeanPropertySqlParameterSource(insertStatement)) + +fun NamedParameterJdbcTemplate.insertMultiple( + insertStatement: MultiRowInsertStatementProvider, + keyHolder: KeyHolder +): Int = + update(insertStatement.insertStatement, BeanPropertySqlParameterSource(insertStatement), keyHolder) + +fun NamedParameterJdbcTemplate.insertSelect(completer: InsertSelectCompleter): Int = + insertSelect(org.mybatis.dynamic.sql.util.kotlin.spring.insertSelect(completer)) + +fun NamedParameterJdbcTemplate.insertSelect(insertStatement: InsertSelectStatementProvider): Int = + update(insertStatement.insertStatement, MapSqlParameterSource(insertStatement.parameters)) + +fun NamedParameterJdbcTemplate.insertSelect( + insertStatement: InsertSelectStatementProvider, + keyHolder: KeyHolder +): Int = + update(insertStatement.insertStatement, MapSqlParameterSource(insertStatement.parameters), keyHolder) + +// insert with KeyHolder support +fun NamedParameterJdbcTemplate.withKeyHolder(keyHolder: KeyHolder, block: KeyHolderHelper.() -> Int): Int = + KeyHolderHelper(keyHolder, this).run(block) -fun NamedParameterJdbcTemplate.insert(record: T, table: SqlTable, completer: InsertCompleter) = - insert(SqlBuilder.insert(record).into(table, completer)) +fun NamedParameterJdbcTemplate.select( + vararg selectList: BasicColumn, + completer: SelectCompleter +): SelectListMapperGatherer = + select(selectList.toList(), completer) -fun NamedParameterJdbcTemplate.select(vararg selectList: BasicColumn) = - SelectListFromGatherer(selectList.toList(), this) +fun NamedParameterJdbcTemplate.select( + selectList: List, + completer: SelectCompleter +): SelectListMapperGatherer = + SelectListMapperGatherer(org.mybatis.dynamic.sql.util.kotlin.spring.select(selectList, completer), this) -fun NamedParameterJdbcTemplate.selectDistinct(vararg selectList: BasicColumn) = - SelectDistinctFromGatherer(selectList.toList(), this) +fun NamedParameterJdbcTemplate.selectDistinct( + vararg selectList: BasicColumn, + completer: SelectCompleter +): SelectListMapperGatherer = + selectDistinct(selectList.toList(), completer) -fun NamedParameterJdbcTemplate.selectOne(vararg selectList: BasicColumn) = - SelectOneFromGatherer(selectList.toList(), this) +fun NamedParameterJdbcTemplate.selectDistinct( + selectList: List, + completer: SelectCompleter +): SelectListMapperGatherer = + SelectListMapperGatherer( + org.mybatis.dynamic.sql.util.kotlin.spring.selectDistinct(selectList, completer), + this + ) -fun NamedParameterJdbcTemplate.selectList(selectStatement: SelectStatementProvider, rowMapper: (rs: ResultSet, rowNum: Int) -> T): List = +fun NamedParameterJdbcTemplate.selectList( + selectStatement: SelectStatementProvider, + rowMapper: (rs: ResultSet, rowNum: Int) -> T +): List = selectList(selectStatement, RowMapper(rowMapper)) + +fun NamedParameterJdbcTemplate.selectList( + selectStatement: SelectStatementProvider, + rowMapper: RowMapper +): List = query(selectStatement.selectStatement, selectStatement.parameters, rowMapper) -fun NamedParameterJdbcTemplate.selectOne(selectStatement: SelectStatementProvider, rowMapper: (rs: ResultSet, rowNum: Int) -> T): T? = +fun NamedParameterJdbcTemplate.selectList( + selectStatement: SelectStatementProvider, + type: KClass +): List = + queryForList(selectStatement.selectStatement, selectStatement.parameters, type.java) + +fun NamedParameterJdbcTemplate.selectOne( + vararg selectList: BasicColumn, + completer: SelectCompleter +): SelectOneMapperGatherer = + selectOne(selectList.toList(), completer) + +fun NamedParameterJdbcTemplate.selectOne( + selectList: List, + completer: SelectCompleter +): SelectOneMapperGatherer = + SelectOneMapperGatherer( + org.mybatis.dynamic.sql.util.kotlin.spring.select(selectList, completer), + this + ) + +fun NamedParameterJdbcTemplate.selectOne( + selectStatement: SelectStatementProvider, + rowMapper: (rs: ResultSet, rowNum: Int) -> T +): T? = selectOne(selectStatement, RowMapper(rowMapper)) + +@SuppressWarnings("SwallowedException") +fun NamedParameterJdbcTemplate.selectOne( + selectStatement: SelectStatementProvider, + rowMapper: RowMapper +): T? = try { queryForObject(selectStatement.selectStatement, selectStatement.parameters, rowMapper) +} catch (e: EmptyResultDataAccessException) { + null +} + +@SuppressWarnings("SwallowedException") +fun NamedParameterJdbcTemplate.selectOne( + selectStatement: SelectStatementProvider, + type: KClass +): T? = try { + queryForObject(selectStatement.selectStatement, selectStatement.parameters, type.java) +} catch (e: EmptyResultDataAccessException) { + null +} -fun NamedParameterJdbcTemplate.update(updateStatement: UpdateStatementProvider) = +fun NamedParameterJdbcTemplate.update(updateStatement: UpdateStatementProvider): Int = update(updateStatement.updateStatement, updateStatement.parameters) -fun NamedParameterJdbcTemplate.update(table: SqlTable, completer: UpdateCompleter) = +fun NamedParameterJdbcTemplate.update(table: SqlTable, completer: UpdateCompleter): Int = update(org.mybatis.dynamic.sql.util.kotlin.spring.update(table, completer)) // support classes for select DSL -class SelectListFromGatherer(private val selectList: List, private val template: NamedParameterJdbcTemplate) { - fun from(table: SqlTable, completer: SelectCompleter) = - SelectListMapperGatherer(SqlBuilder.select(selectList).from(table, completer), template) +@MyBatisDslMarker +class SelectListMapperGatherer( + private val selectStatement: SelectStatementProvider, + private val template: NamedParameterJdbcTemplate +) { + fun withRowMapper(rowMapper: (rs: ResultSet, rowNum: Int) -> T): List = + template.selectList(selectStatement, rowMapper) - fun from(table: SqlTable, alias: String, completer: SelectCompleter) = - SelectListMapperGatherer(SqlBuilder.select(selectList).from(table, alias, completer), template) + fun withRowMapper(rowMapper: RowMapper): List = + template.selectList(selectStatement, rowMapper) } -class SelectDistinctFromGatherer(private val selectList: List, private val template: NamedParameterJdbcTemplate) { - fun from(table: SqlTable, completer: SelectCompleter) = - SelectListMapperGatherer(SqlBuilder.selectDistinct(selectList).from(table, completer), template) +@MyBatisDslMarker +class SelectOneMapperGatherer( + private val selectStatement: SelectStatementProvider, + private val template: NamedParameterJdbcTemplate +) { + fun withRowMapper(rowMapper: (rs: ResultSet, rowNum: Int) -> T): T? = + template.selectOne(selectStatement, rowMapper) - fun from(table: SqlTable, alias: String, completer: SelectCompleter) = - SelectListMapperGatherer(SqlBuilder.selectDistinct(selectList).from(table, alias, completer), template) + fun withRowMapper(rowMapper: RowMapper): T? = + template.selectOne(selectStatement, rowMapper) } -class SelectOneFromGatherer(private val selectList: List, private val template: NamedParameterJdbcTemplate) { - fun from(table: SqlTable, completer: SelectCompleter) = - SelectOneMapperGatherer(SqlBuilder.select(selectList).from(table, completer), template) +@MyBatisDslMarker +class KeyHolderHelper(private val keyHolder: KeyHolder, private val template: NamedParameterJdbcTemplate) { + fun insertInto(table: SqlTable, completer: GeneralInsertCompleter): Int = + template.generalInsert(org.mybatis.dynamic.sql.util.kotlin.spring.insertInto(table, completer), keyHolder) - fun from(table: SqlTable, alias: String, completer: SelectCompleter) = - SelectOneMapperGatherer(SqlBuilder.select(selectList).from(table, alias, completer), template) -} + fun insert(row: T, completer: KotlinInsertCompleter): Int = + template.insert(org.mybatis.dynamic.sql.util.kotlin.spring.insert(row, completer), keyHolder) -class SelectListMapperGatherer(private val selectStatement: SelectStatementProvider, private val template: NamedParameterJdbcTemplate) { - fun withRowMapper(rowMapper: (rs: ResultSet, rowNum: Int) -> T) = - template.selectList(selectStatement, rowMapper) -} + fun insertMultiple(vararg records: T, completer: KotlinMultiRowInsertCompleter): Int = + insertMultiple(records.asList(), completer) -class SelectOneMapperGatherer(private val selectStatement: SelectStatementProvider, private val template: NamedParameterJdbcTemplate) { - fun withRowMapper(rowMapper: (rs: ResultSet, rowNum: Int) -> T) = - template.selectOne(selectStatement, rowMapper) + fun insertMultiple(records: List, completer: KotlinMultiRowInsertCompleter): Int = + template.insertMultiple(org.mybatis.dynamic.sql.util.kotlin.spring.insertMultiple(records, completer), + keyHolder) + + fun insertSelect(completer: InsertSelectCompleter): Int = + template.insertSelect(org.mybatis.dynamic.sql.util.kotlin.spring.insertSelect(completer), keyHolder) } diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/spring/ProviderBuilderFunctions.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/spring/ProviderBuilderFunctions.kt index e3a032135..895bbaa20 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/spring/ProviderBuilderFunctions.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/spring/ProviderBuilderFunctions.kt @@ -1,11 +1,11 @@ -/** - * Copyright 2016-2019 the original author or authors. +/* + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -13,49 +13,93 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:Suppress("TooManyFunctions") package org.mybatis.dynamic.sql.util.kotlin.spring -import org.mybatis.dynamic.sql.SqlBuilder +import org.mybatis.dynamic.sql.BasicColumn import org.mybatis.dynamic.sql.SqlTable import org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider -import org.mybatis.dynamic.sql.insert.InsertDSL +import org.mybatis.dynamic.sql.insert.render.BatchInsert +import org.mybatis.dynamic.sql.insert.render.GeneralInsertStatementProvider +import org.mybatis.dynamic.sql.insert.render.InsertSelectStatementProvider import org.mybatis.dynamic.sql.insert.render.InsertStatementProvider +import org.mybatis.dynamic.sql.insert.render.MultiRowInsertStatementProvider import org.mybatis.dynamic.sql.render.RenderingStrategies -import org.mybatis.dynamic.sql.select.QueryExpressionDSL -import org.mybatis.dynamic.sql.select.SelectModel import org.mybatis.dynamic.sql.select.render.SelectStatementProvider import org.mybatis.dynamic.sql.update.render.UpdateStatementProvider -import org.mybatis.dynamic.sql.util.kotlin.* - -fun countFrom(table: SqlTable, completer: CountCompleter): SelectStatementProvider { - val builder = KotlinCountBuilder(SqlBuilder.countFrom(table)) - completer(builder) - return builder.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER) -} - -fun deleteFrom(table: SqlTable, completer: DeleteCompleter): DeleteStatementProvider { - val builder = KotlinDeleteBuilder(SqlBuilder.deleteFrom(table)) - completer(builder) - return builder.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER) -} - -fun InsertDSL.IntoGatherer.into(table: SqlTable, completer: InsertCompleter): InsertStatementProvider = - completer(into(table)).build().render(RenderingStrategies.SPRING_NAMED_PARAMETER) - -fun QueryExpressionDSL.FromGatherer.from(table: SqlTable, completer: SelectCompleter): SelectStatementProvider { - val builder = KotlinQueryBuilder(from(table)) - completer(builder) - return builder.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER) -} - -fun QueryExpressionDSL.FromGatherer.from(table: SqlTable, alias: String, completer: SelectCompleter): SelectStatementProvider { - val builder = KotlinQueryBuilder(from(table, alias)) - completer(builder) - return builder.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER) -} - -fun update(table: SqlTable, completer: UpdateCompleter): UpdateStatementProvider { - val builder = KotlinUpdateBuilder(SqlBuilder.update(table)) - completer(builder) - return builder.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER) -} +import org.mybatis.dynamic.sql.util.kotlin.CountCompleter +import org.mybatis.dynamic.sql.util.kotlin.DeleteCompleter +import org.mybatis.dynamic.sql.util.kotlin.GeneralInsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.InsertSelectCompleter +import org.mybatis.dynamic.sql.util.kotlin.KotlinBatchInsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.KotlinInsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.KotlinMultiRowInsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.MultiSelectCompleter +import org.mybatis.dynamic.sql.util.kotlin.SelectCompleter +import org.mybatis.dynamic.sql.util.kotlin.UpdateCompleter +import org.mybatis.dynamic.sql.util.kotlin.model.count +import org.mybatis.dynamic.sql.util.kotlin.model.countDistinct +import org.mybatis.dynamic.sql.util.kotlin.model.countFrom +import org.mybatis.dynamic.sql.util.kotlin.model.deleteFrom +import org.mybatis.dynamic.sql.util.kotlin.model.insert +import org.mybatis.dynamic.sql.util.kotlin.model.insertBatch +import org.mybatis.dynamic.sql.util.kotlin.model.insertInto +import org.mybatis.dynamic.sql.util.kotlin.model.insertMultiple +import org.mybatis.dynamic.sql.util.kotlin.model.insertSelect +import org.mybatis.dynamic.sql.util.kotlin.model.select +import org.mybatis.dynamic.sql.util.kotlin.model.selectDistinct +import org.mybatis.dynamic.sql.util.kotlin.model.update + +fun count(column: BasicColumn, completer: CountCompleter): SelectStatementProvider = + count(column, completer).render(RenderingStrategies.SPRING_NAMED_PARAMETER) + +fun countDistinct(column: BasicColumn, completer: CountCompleter): SelectStatementProvider = + countDistinct(column, completer).render(RenderingStrategies.SPRING_NAMED_PARAMETER) + +fun countFrom(table: SqlTable, completer: CountCompleter): SelectStatementProvider = + countFrom(table, completer).render(RenderingStrategies.SPRING_NAMED_PARAMETER) + +fun deleteFrom(table: SqlTable, completer: DeleteCompleter): DeleteStatementProvider = + deleteFrom(table, completer).render(RenderingStrategies.SPRING_NAMED_PARAMETER) + +fun deleteFrom(table: SqlTable, tableAlias: String, completer: DeleteCompleter): DeleteStatementProvider = + deleteFrom(table, tableAlias, completer).render(RenderingStrategies.SPRING_NAMED_PARAMETER) + +fun insert(row: T, completer: KotlinInsertCompleter): InsertStatementProvider = + insert(row, completer).render(RenderingStrategies.SPRING_NAMED_PARAMETER) + +fun insertBatch(rows: Collection, completer: KotlinBatchInsertCompleter): BatchInsert = + insertBatch(rows, completer).render(RenderingStrategies.SPRING_NAMED_PARAMETER) + +fun insertInto(table: SqlTable, completer: GeneralInsertCompleter): GeneralInsertStatementProvider = + insertInto(table, completer).render(RenderingStrategies.SPRING_NAMED_PARAMETER) + +fun insertMultiple( + rows: Collection, + completer: KotlinMultiRowInsertCompleter +): MultiRowInsertStatementProvider = + insertMultiple(rows, completer).render(RenderingStrategies.SPRING_NAMED_PARAMETER) + +fun insertSelect(completer: InsertSelectCompleter): InsertSelectStatementProvider = + insertSelect(completer).render(RenderingStrategies.SPRING_NAMED_PARAMETER) + +fun select(vararg columns: BasicColumn, completer: SelectCompleter): SelectStatementProvider = + select(columns.asList(), completer).render(RenderingStrategies.SPRING_NAMED_PARAMETER) + +fun select(columns: List, completer: SelectCompleter): SelectStatementProvider = + select(columns, completer).render(RenderingStrategies.SPRING_NAMED_PARAMETER) + +fun selectDistinct(vararg columns: BasicColumn, completer: SelectCompleter): SelectStatementProvider = + selectDistinct(columns.asList(), completer).render(RenderingStrategies.SPRING_NAMED_PARAMETER) + +fun selectDistinct(columns: List, completer: SelectCompleter): SelectStatementProvider = + selectDistinct(columns, completer).render(RenderingStrategies.SPRING_NAMED_PARAMETER) + +fun multiSelect(completer: MultiSelectCompleter): SelectStatementProvider = + org.mybatis.dynamic.sql.util.kotlin.model.multiSelect(completer).render(RenderingStrategies.SPRING_NAMED_PARAMETER) + +fun update(table: SqlTable, completer: UpdateCompleter): UpdateStatementProvider = + update(table, completer).render(RenderingStrategies.SPRING_NAMED_PARAMETER) + +fun update(table: SqlTable, tableAlias: String, completer: UpdateCompleter): UpdateStatementProvider = + update(table, tableAlias, completer).render(RenderingStrategies.SPRING_NAMED_PARAMETER) diff --git a/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties b/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties new file mode 100644 index 000000000..89ea08e70 --- /dev/null +++ b/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties @@ -0,0 +1,71 @@ +# +# Copyright 2016-2025 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# 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. +# + +ERROR.1=Table "{0}" with requested alias "{1}" is already aliased in this query with alias "{2}". Attempting to \ + re-alias a table in the same query is not supported +ERROR.2=A where clause was specified, but failed to render +ERROR.3=IOException reading property file "{0}" +ERROR.4=Insert select statements require at least one column in the column list +ERROR.5=Batch insert statements must have at least one column mapping +ERROR.6=General insert statements must have at least one column mapping +ERROR.7=Insert statements must have at least one column mapping +ERROR.8=Multiple row insert statements must have at least one column mapping +ERROR.9=All set phrases were dropped when rendering the general insert statement +ERROR.10=All column mappings were dropped when rendering the insert statement +ERROR.11=Group by expressions must have at least one column +ERROR.12=Order by expressions must have at least one column +ERROR.13=Query expressions must have at least one column in the select list +ERROR.14=Select statements must have at least one query expression +ERROR.15=Joins must have at least one join specification +ERROR.16=Join specifications must have at least one join criterion +ERROR.17=Update statements must have at least one set phrase +ERROR.18=All set phrases were dropped when rendering the update statement +ERROR.19=Batch insert statements must have at least one record to insert +ERROR.20=Multiple row insert statements must have at least one record to insert +ERROR.21=Setting more than one initial criterion is not allowed. Additional criteria should be added with an "and" \ + or "or" expression +ERROR.22=You must specify an "on" condition in a join +ERROR.23=Batch Insert Statements Must Contain an "into" phrase +ERROR.24=You must specify a "from" clause before any other clauses in a count statement +ERROR.25=Insert Statements Must Contain an "into" phrase +ERROR.26=Multiple Row Insert Statements Must Contain an "into" phrase +ERROR.27=You must specify a "from" clause before any other clauses in a select statement +ERROR.28=You must specify a select statement in a sub query +ERROR.29=Insert Select Statements Must Contain an "into" phrase +ERROR.30=The parameters for insertMultipleWithGeneratedKeys must contain exactly one parameter of type String +ERROR.31=You cannot specify more than one "having" clause in a query expression +ERROR.32=You cannot specify more than one "where" clause in a statement +ERROR.33=Calling "select" or "selectDistinct" more than once is not allowed. Additional queries should be added with a \ + union or unionAll expression +ERROR.34=You must specify "select" or "selectDistinct" before any other clauses in a multi-select statement +ERROR.35=Multi-select statements must have at least one "union" or "union all" expression +ERROR.36=Obsolete Message - Not Used +ERROR.37=The "{0}" function does not support conditions that fail to render +ERROR.38=Bound values cannot be aliased +ERROR.39=When clauses in case expressions must render +ERROR.40=Case expressions must have at least one "when" clause +ERROR.41=You cannot call "then" in a Kotlin case expression more than once +ERROR.42=You cannot call `else` in a Kotlin case expression more than once +ERROR.43=A Kotlin cast expression must have one, and only one, `as` element +ERROR.44={0} conditions must contain at least one value +ERROR.45=You cannot call "on" in a Kotlin join expression more than once +ERROR.46=At least one join criterion must render +ERROR.47=A Kotlin case statement must specify a "then" clause for every "when" clause +ERROR.48=You cannot call more than one of "forUpdate", "forNoKeyUpdate", "forShare", or "forKeyShare" in a select \ + statement +ERROR.49=You cannot call more than one of "skipLocked", or "nowait" in a select statement +ERROR.50=Mapped column {0} does not have a javaProperty configured +INTERNAL.ERROR=Internal Error {0} diff --git a/src/site/markdown/docs/caseExpressions.md b/src/site/markdown/docs/caseExpressions.md new file mode 100644 index 000000000..3f711e626 --- /dev/null +++ b/src/site/markdown/docs/caseExpressions.md @@ -0,0 +1,196 @@ +# Case Expressions in the Java DSL + +Support for case expressions was added in version 1.5.1. For information about case expressions in the Kotlin DSL, see +the [Kotlin Case Expressions](kotlinCaseExpressions.md) page. + +## Case Expressions in SQL +The library supports different types of case expressions - a "simple" case expression, and a "searched" case +expressions. Case expressions can be used in many places including select lists, order by phrases, etc. + +A simple case expression checks the values of a single column. It looks like this: + +```sql +select case id + when 1, 2, 3 then true + else false + end as small_id +from foo +``` + +Some databases also support simple comparisons on simple case expressions, which look lke this: + +```sql +select case total_length + when < 10 then 'small' + when > 20 then 'large' + else 'medium' + end as tshirt_size +from foo +``` + +A searched case expression allows arbitrary logic, and it can check the values of multiple columns. It looks like this: + +```sql +select case + when animal_name = 'Small brown bat' or animal_name = 'Large brown bat' then 'Bat' + when animal_name = 'Artic fox' or animal_name = 'Red fox' then 'Fox' + else 'Other' + end as animal_type +from foo +``` + +## Bind Variables and Casting + +The library will always render the "when" part of a case expression using bind variables. Rendering of the "then" and +"else" parts of a case expression may or may not use bind variables depending on how you write the query. In general, +the library will render "then" and "else" as constants - meaning not using bind variables. If you wish to use bind +variables for these parts of a case expressions, then you can use the `value` function to turn a constant into a +bind variable. We will show examples of the different renderings in the following sections. + +If you choose to use bind variables for all "then" and "else" values, it is highly likely that the database will +require you to specify an expected datatype by using a `cast` function. + +Even for "then" and "else" sections that are rendered with constants, you may still desire to use a `cast` in some +cases. For example, if you specify Strings for all "then" and "else" values, the database will likely return all +values as datatype CHAR with the length of the longest constant string. Typically, we would prefer the use of VARCHAR, +so we don't have to strip trailing blanks from the results. This is a good use for a `cast` with a constant. +Similarly, Java float constants are often interpreted by databases as BigDecimal. You can use a `cast` to have them +returned as floats. + +Note: in the following sections we will use `?` to show a bind variable, but the actual rendered SQL will be different +because bind variables will be rendered appropriately for the execution engine you are using (either MyBatis or Spring). + +Also note: in Java, `case` and `else` are reserved words - meaning we cannot use them as method names. For this reason, +the library uses `case_` and `else_` respectively as method names. + +Full examples for case expressions are in the test code for the library here: +https://github.com/mybatis/mybatis-dynamic-sql/blob/master/src/test/java/examples/animal/data/CaseExpressionTest.java + +## Java DSL for Simple Case Statements with Simple Values + +A simple case expression can be coded like the following in the Java DSL: + +```java +select(case_(id) + .when(1, 2, 3).then(true) + .else_(false) + .end().as("small_id")) +.from(foo) +``` + +A statement written this way will render as follows: + +```sql +select case id when ?, ?, ? then true else false end as small_id from foo +``` + +Note that the "then" and "else" parts are NOT rendered with bind variables. If you with to use bind variables, then +you can write the query as follows: + +```java +select(case_(id) + .when(1, 2, 3).then(value(true)) + .else_(value(false)) + .end().as("small_id")) +.from(foo) +``` + +In this case, we are using the `value` function to denote a bind variable. The SQL will now be rendered as follows: + +```sql +select case id when ?, ?, ? then ? else ? end as small_id from foo +``` + +*Important*: Be aware that your database may throw an exception for SQL like this because the database cannot determine +the datatype of the resulting column. If that happens, you will need to cast one or more of the variables to the +expected data type. Here's an example of using the `cast` function: + +```java +select(case_(id) + .when(1, 2, 3).then(value(true)) + .else_(cast(value(false)).as("BOOLEAN)")) + .end().as("small_id")) +.from(foo) +``` + +In this case, the SQL will render as follows: + +```sql +select case id when ?, ?, ? then ? else cast(? as BOOLEAN) end as small_id from foo +``` + +In our testing, casting a single bound value is enough to inform the database of your expected datatype, but +you should perform your own testing. + +## Java DSL for Simple Case Statements with Conditions + +A simple case expression can be coded like the following in the Java DSL: + +```java +select(case_(total_length) + .when(isLessThan(10)).then_("small") + .when(isGreaterThan(20)).then_("large") + .else_("medium") + .end().as("tshirt_size")) +.from(foo) +``` + +A statement written this way will render as follows: + +```sql +select case total_length when < ? then 'small' when > ? then 'large' else 'medium' end as tshirt_size from foo +``` + +Note that the "then" and "else" parts are NOT rendered with bind variables. If you with to use bind variables, then +you can use the `value` function as shown above. + +A query like this could be a good place to use casting with constants. Most databases will return the calculated +"tshirt_size" column as CHAR(6) - so the "small" and "large" values will have a trailing blank. If you wish to use +VARCHAR, you can use the `cast` function as follows: + +```java +select(case_(total_length) + .when(isLessThan(10)).then_("small") + .when(isGreaterThan(20)).then_("large") + .else_(cast("medium").as("VARCHAR(6)")) + .end().as("tshirt_size")) +.from(foo) +``` + +In this case, we are using the `cast` function to specify the datatype of a constant. The SQL will now be rendered as +follows (without the line breaks): + +```sql +select case total_length + when < ? then 'small' when > ? then 'large' + else cast('medium' as VARCHAR(6)) end as tshirt_size from foo +``` + +## Java DSL for Searched Case Statements + +A searched case statement is written as follows: + +```java +select(case_() + .when(animalName, isEqualTo("Small brown bat")).or(animalName, isEqualTo("Large brown bat")).then("Bat") + .when(animalName, isEqualTo("Artic fox")).or(animalName, isEqualTo("Red fox")).then("Fox") + .else_("Other") + .end().as("animal_type")) +.from(foo) +``` + +The full syntax of "where" and "having" clauses is supported in the "when" clause - but that may or may not be supported +by your database. Testing is crucial. The library supports optional conditions in "when" clauses, but at least one +condition must render, else the library will throw an `InvalidSqlException`. + +The rendered SQL will be as follows (without the line breaks): +```sql +select case + when animal_name = ? or animal_name = ? then 'Bat' + when animal_name = ? or animal_name = ? then 'Fox' + else 'Other' + end as animal_type +from foo +``` + +The use of the `value` function to support bind variables, and the use of casting, is the same is shown above. diff --git a/src/site/markdown/docs/codingStandards.md b/src/site/markdown/docs/codingStandards.md index 29a26d860..bfb0c5404 100644 --- a/src/site/markdown/docs/codingStandards.md +++ b/src/site/markdown/docs/codingStandards.md @@ -20,8 +20,8 @@ these general principles for functional style coding in Java: - Classes never expose a modifiable Map. A Map may be exposed with an unmodifiable Map. - Avoid direct use of null. Any Class attribute that could be null in normal use should be wrapped in a `java.util.Optional` - Avoid for loops (imperative) - use map/filter/reduce/collect (declarative) instead -- Avoid Stream.forEach() - this method is only used for side effects, and we want no side-effects -- Avoid Optional.ifPresent() - this method is only used for side effects, and we want no side-effects +- Avoid Stream.forEach() - this method is only used for side effects, and we want no side effects +- Avoid Optional.ifPresent() - this method is only used for side effects, and we want no side effects - The only good function is a pure function. Some functions in the library accept an AtomicInteger which is a necessary evil - Classes with no internal attributes are usually a collection of utility functions. Use static methods in an interface instead. - Remember the single responsibility principle - methods do one thing, classes have one responsibility @@ -30,7 +30,7 @@ these general principles for functional style coding in Java: We are committed to clean code. This means: -- Small methods - less than 5 lines is good, 1 line is ideal +- Small methods - less than 5 lines is good, 1 line is ideal - Small classes - less than 100 lines is good, less than 50 lines is ideal - Use descriptive names - Comments are a last resort - don't comment bad code, refactor it @@ -43,7 +43,7 @@ We are committed to clean code. This means: Remember the three rules of TDD: 1. You may not write production code until you have written a failing unit test. -2. You may not write more of a unit test that is sufficient to fail, and not compiling is failing. +2. You may not write more of a unit test than is sufficient to fail, and not compiling is failing. 3. You may not write more production code than is sufficient to passing the currently failing test. diff --git a/src/site/markdown/docs/complexQueries.md b/src/site/markdown/docs/complexQueries.md index f603727e0..36614324b 100644 --- a/src/site/markdown/docs/complexQueries.md +++ b/src/site/markdown/docs/complexQueries.md @@ -1,7 +1,13 @@ # Complex Queries -Enhancements in version 1.1.2 make it easier to code complex queries. The Select DSL is implemented as a set of related objects. As the select statement is built, intermediate objects of various types are returned from the various methods that implement the DSL. The select statement can be completed by calling the `build()` method many of the intermediate objects. Prior to version 1.1.2, it was necessary to call `build()` on the **last** intermediate object. This restriction has been removed and it is now possible to call `build()` on **any** intermediate object. This, along with several other enhancements, has simplified the coding of complex queries. +Enhancements in version 1.1.2 make it easier to code complex queries. The Select DSL is implemented as a set of related +objects. As the select statement is built, intermediate objects of various types are returned from the various methods +that implement the DSL. The select statement can be completed by calling the `build()` method many of the intermediate +objects. Prior to version 1.1.2, it was necessary to call `build()` on the **last** intermediate object. This +restriction has been removed, and it is now possible to call `build()` on **any** intermediate object. This, along with +several other enhancements, has simplified the coding of complex queries. -For example, suppose you want to code a complex search on a Person table. The search parameters are id, first name, and last name. The rules are: +For example, suppose you want to code a complex search on a Person table. The search parameters are id, first name, +and last name. The rules are: 1. If an id is entered, use the id and ignore the other search parameters 1. If an id is not entered, then do a fuzzy search based on the other parameters @@ -13,23 +19,23 @@ public SelectStatementProvider search(Integer targetId, String fName, String lNa var builder = select(id, firstName, lastName) // (1) .from(person) .where(); // (2) - + if (targetId != null) { // (3) builder .and(id, isEqualTo(targetId)); } else { builder - .and(firstName, isLike(fName).when(Objects::nonNull).then(s -> "%" + s + "%")) // (4) - .and(lastName, isLikeWhenPresent(lName).then(this::addWildcards)); // (5) + .and(firstName, isLike(fName).filter(Objects::nonNull).map(s -> "%" + s + "%")) // (4) (5) + .and(lastName, isLikeWhenPresent(lName).map(this::addWildcards)); // (6) } builder .orderBy(lastName, firstName) - .fetchFirst(50).rowsOnly(); // (6) - - return builder.build().render(RenderingStrategies.MYBATIS3); // (7) + .fetchFirst(50).rowsOnly(); // (7) + + return builder.build().render(RenderingStrategies.MYBATIS3); // (8) } - + public String addWildcards(String s) { return "%" + s + "%"; } @@ -37,11 +43,21 @@ public String addWildcards(String s) { Notes: -1. Note the use of the `var` keyword here. If you are using an older version of Java, the actual type is `QueryExpressionDSL.QueryExpressionWhereBuilder` -1. Here we are calling `where()` with no parameters. This sets up the builder to accept conditions further along in the code. If no conditions are added, then the where clause will not be rendered -1. This `if` statement implements the rules of the search. If an ID is entered , use it. Otherwise do a fuzzy search based on first name and last name. -1. The `then` statement on this line allows you to change the parameter value before it is placed in the parameter Map. In this case we are adding SQL wildcards to the start and end of the search String - but only if the search String is not null. If the search String is null, the lambda will not be called and the condition will not render -1. This shows using a method reference instead of a lambda on the `then`. Method references allow you to more clearly express intent. Note also the use of the `isLikeWhenPresent` condition which is a built in condition that checks for nulls -1. It is a good idea to limit the number of rows returned from a search. The library now supports `fetch first` syntax for limiting rows -1. Note that we are calling the `build` method from the intermediate object retrieved in step 1. It is no longer necessary to call `build` on the last object returned from a select builder +1. Note the use of the `var` keyword here. If you are using an older version of Java, the actual type is + `QueryExpressionDSL.QueryExpressionWhereBuilder` +1. Here we are calling `where()` with no parameters. This sets up the builder to accept conditions further along in the + code. If no conditions are added, then the where clause will not be rendered +1. This `if` statement implements the rules of the search. If an ID is entered , use it. Otherwise, do a fuzzy search + based on first name and last name. +1. The `filter` method on this line will mark the condition as unrenderable if the filter is not satisfied. +1. The `map` statement on this line allows you to change the parameter value before it is placed in the parameter Map. + In this case we are adding SQL wildcards to the start and end of the search String - but only if the search String + is not null. If the search String is null, the lambda will not be called and the condition will not render +1. This line shows the use of a method reference instead of a lambda on the `map`. Method references allow you to more + clearly express intent. Note also the use of the `isLikeWhenPresent` function which is a built-in function that + applies a non-null filter +1. It is a good idea to limit the number of rows returned from a search. The library now supports `fetch first` syntax + for limiting rows +1. Note that we are calling the `build` method from the intermediate object retrieved in step 1. It is no longer + necessary to call `build` on the last object returned from a select builder diff --git a/src/site/markdown/docs/conditions.md b/src/site/markdown/docs/conditions.md index 71648bb7d..438249400 100644 --- a/src/site/markdown/docs/conditions.md +++ b/src/site/markdown/docs/conditions.md @@ -11,60 +11,116 @@ In the following examples: Simple conditions are the most common - they render the basic SQL operators. -| Condition | Example | Result | -|-----------|---------|--------| -| Between | where(foo, isBetween(x).and(y)) | `where foo between ? and ?` | -| Equals | where(foo, isEqualTo(x)) | `where foo = ?` | -| Greater Than | where(foo, isGreaterThan(x)) | `where foo > ?` | -| Greater Than or Equals | where(foo, isGreaterThanOrEqualTo(x)) | `where foo >= ?` | -| In | where(foo, isIn(x, y)) | `where foo in (?,?)` | -| In (case insensitive) | where(foo, isInCaseInsensitive(x, y)) | `where upper(foo) in (?,?)` (the framework will transform the values for x and y to upper case)| -| Less Than | where(foo, isLessThan(x)) | `where foo < ?` | -| Less Than or Equals | where(foo, isLessThanOrEqualTo(x)) | `where foo <= ?` | -| Like | where(foo, isLike(x)) | `where foo like ?` (the framework DOES NOT add the SQL wild cards to the value - you will need to do that yourself) | -| Like (case insensitive) | where(foo, isLikeCaseInsensitive(x)) | `where upper(foo) like ?` (the framework DOES NOT add the SQL wild cards to the value - you will need to do that yourself, the framework will transform the value of x to upper case) | -| Not Between | where(foo, isNotBetween(x).and(y)) | `where foo not between ? and ?` | -| Not Equals | where(foo, isNotEqualTo(x)) | `where foo <> ?` | -| Not In | where(foo, isNotIn(x, y)) | `where foo not in (?,?)` | -| Not In (case insensitive) | where(foo, isNotInCaseInsensitive(x, y)) | `where upper(foo) not in (?,?)` (the framework will transform the values for x and y to upper case)| -| Not Like | where(foo, isLike(x)) | `where foo not like ?` (the framework DOES NOT add the SQL wild cards to the value - you will need to do that yourself) | -| Not Like (case insensitive) | where(foo, isNotLikeCaseInsensitive(x)) | `where upper(foo) not like ?` (the framework DOES NOT add the SQL wild cards to the value - you will need to do that yourself, the framework will transform the value of x to upper case) | -| Not Null | where(foo, isNotNull()) | `where foo is not null` | -| Null | where(foo, isNull()) | `where foo is null` | - - -## Sub-Selects - -Many conditions can be rendered with sub selects. - -| Condition | Example | Result | -|-----------|---------|--------| -| Equals | where(foo, isEqualTo(select(bar).from(table2).where(bar, isEqualTo(x))) | `where foo = (select bar from table2 where bar = ?)` | -| Greater Than | where(foo, isGreaterThan(select(bar).from(table2).where(bar, isEqualTo(x))) | `where foo > (select bar from table2 where bar = ?)` | -| Greater Than or Equals | where(foo, isGreaterThanOrEqualTo(select(bar).from(table2).where(bar, isEqualTo(x))) | `where foo >= (select bar from table2 where bar = ?)` | -| In | where(foo, isIn(select(bar).from(table2).where(bar, isLessThan(x))) | `where foo in (select bar from table2 where bar < ?)` | -| Less Than | where(foo, isLessThan(select(bar).from(table2).where(bar, isEqualTo(x))) | `where foo < (select bar from table2 where bar = ?)` | -| Less Than or Equals | where(foo, isLessThanOrEqualTo(select(bar).from(table2).where(bar, isEqualTo(x))) | `where foo <= (select bar from table2 where bar = ?)` | -| Not Equals | where(foo, isNotEqualTo(select(bar).from(table2).where(bar, isEqualTo(x))) | `where foo <> (select bar from table2 where bar = ?)` | -| Not In | where(foo, isNotIn(select(bar).from(table2).where(bar, isLessThan(x))) | `where foo not in (select bar from table2 where bar < ?)` | +| Condition | Example | Result | +|-----------------------------|------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Between | where(foo, isBetween(x).and(y)) | `where foo between ? and ?` | +| Equals | where(foo, isEqualTo(x)) | `where foo = ?` | +| Greater Than | where(foo, isGreaterThan(x)) | `where foo > ?` | +| Greater Than or Equals | where(foo, isGreaterThanOrEqualTo(x)) | `where foo >= ?` | +| In | where(foo, isIn(x, y)) | `where foo in (?,?)` | +| In (case insensitive) | where(foo, isInCaseInsensitive(x, y)) | `where upper(foo) in (?,?)` (the framework will transform the values for x and y to upper case) | +| Less Than | where(foo, isLessThan(x)) | `where foo < ?` | +| Less Than or Equals | where(foo, isLessThanOrEqualTo(x)) | `where foo <= ?` | +| Like | where(foo, isLike(x)) | `where foo like ?` (the framework DOES NOT add the SQL wild cards to the value - you will need to do that yourself) | +| Like (case insensitive) | where(foo, isLikeCaseInsensitive(x)) | `where upper(foo) like ?` (the framework DOES NOT add the SQL wild cards to the value - you will need to do that yourself, the framework will transform the value of x to upper case) | +| Not Between | where(foo, isNotBetween(x).and(y)) | `where foo not between ? and ?` | +| Not Equals | where(foo, isNotEqualTo(x)) | `where foo <> ?` | +| Not In | where(foo, isNotIn(x, y)) | `where foo not in (?,?)` | +| Not In (case insensitive) | where(foo, isNotInCaseInsensitive(x, y)) | `where upper(foo) not in (?,?)` (the framework will transform the values for x and y to upper case) | +| Not Like | where(foo, isNotLike(x)) | `where foo not like ?` (the framework DOES NOT add the SQL wild cards to the value - you will need to do that yourself) | +| Not Like (case insensitive) | where(foo, isNotLikeCaseInsensitive(x)) | `where upper(foo) not like ?` (the framework DOES NOT add the SQL wild cards to the value - you will need to do that yourself, the framework will transform the value of x to upper case) | +| Not Null | where(foo, isNotNull()) | `where foo is not null` | +| Null | where(foo, isNull()) | `where foo is null` | + + +## Subqueries + +Many conditions can be rendered with subqueries. + +| Condition | Example | Result | +|-------------------------|--------------------------------------------------------------------------------------|-----------------------------------------------------------| +| Equals | where(foo, isEqualTo(select(bar).from(table2).where(bar, isEqualTo(x))) | `where foo = (select bar from table2 where bar = ?)` | +| Greater Than | where(foo, isGreaterThan(select(bar).from(table2).where(bar, isEqualTo(x))) | `where foo > (select bar from table2 where bar = ?)` | +| Greater Than or Equals | where(foo, isGreaterThanOrEqualTo(select(bar).from(table2).where(bar, isEqualTo(x))) | `where foo >= (select bar from table2 where bar = ?)` | +| In | where(foo, isIn(select(bar).from(table2).where(bar, isLessThan(x))) | `where foo in (select bar from table2 where bar < ?)` | +| Less Than | where(foo, isLessThan(select(bar).from(table2).where(bar, isEqualTo(x))) | `where foo < (select bar from table2 where bar = ?)` | +| Less Than or Equals | where(foo, isLessThanOrEqualTo(select(bar).from(table2).where(bar, isEqualTo(x))) | `where foo <= (select bar from table2 where bar = ?)` | +| Not Equals | where(foo, isNotEqualTo(select(bar).from(table2).where(bar, isEqualTo(x))) | `where foo <> (select bar from table2 where bar = ?)` | +| Not In | where(foo, isNotIn(select(bar).from(table2).where(bar, isLessThan(x))) | `where foo not in (select bar from table2 where bar < ?)` | ## Column Comparison Conditions Column comparison conditions can be used to write where clauses comparing the values of columns in a table. -| Condition | Example | Result | -|-----------|---------|--------| -| Equals | where(foo, isEqualTo(bar)) | `where foo = bar` | -| Greater Than | where(foo, isGreaterThan(bar)) | `where foo > bar` | +| Condition | Example | Result | +|------------------------|-----------------------------------------|--------------------| +| Equals | where(foo, isEqualTo(bar)) | `where foo = bar` | +| Greater Than | where(foo, isGreaterThan(bar)) | `where foo > bar` | | Greater Than or Equals | where(foo, isGreaterThanOrEqualTo(bar)) | `where foo >= bar` | -| Less Than | where(foo, isLessThan(bar)) | `where foo < bar` | -| Less Than or Equals | where(foo, isLessThanOrEqualTo(bar)) | `where foo <= bar` | -| Not Equals | where(foo, isNotEqualTo(bar)) | `where foo <> bar` | +| Less Than | where(foo, isLessThan(bar)) | `where foo < bar` | +| Less Than or Equals | where(foo, isLessThanOrEqualTo(bar)) | `where foo <= bar` | +| Not Equals | where(foo, isNotEqualTo(bar)) | `where foo <> bar` | + +## Value Transformation + +All conditions (except `isNull` and `isNotNull`) support a `map` function that allows you to transform the value(s) +associated with the condition before the statement is rendered. The map function functions similarly to the JDK +standard `map` functions on Streams and Optionals - it allows you to transform a value and change the data type. + +For example, suppose you want to code a wild card search with the SQL `like` operator. To make this work, you will +need to append SQL wildcards to the search value. This can be accomplished directly in the condition with a `map` +method as follows: + +```java +List search(String searchName) { + SelectStatementProvider selectStatement=select(id,animalName,bodyWeight,brainWeight) + .from(animalData) + .where(animalName,isLike(searchName).map(s -> "%"+s+"%")) + .orderBy(id) + .build() + .render(RenderingStrategies.MYBATIS3); + + ... +} +``` + +You can see the `map` method accepts a lambda that adds SQL wildcards to the `searchName` field. This is more succinct +if you use a method reference: + +```java +List search(String searchName){ + SelectStatementProvider selectStatement=select(id,animalName,bodyWeight,brainWeight) + .from(animalData) + .where(animalName,isLike(searchName).map(this::appendWildCards)) + .orderBy(id) + .build() + .render(RenderingStrategies.MYBATIS3); +} + +String appendWildCards(String in) { + return "%" + in + "%"; +} +``` + +The `map` on each condition accepts a lambda expression that can be used to transform the value(s) associated with the +condition. The lambda is the standard JDK type `Function<T,R>` where `T` is the type of the condition and `R` +is the output type. For most conditions this should be fairly simple to understand. The unusual cases are detailed +below: + +1. The `Between` and `NotBetween` conditions have `map` methods that accept one or two map functions. If you pass + one function, it will be applied to both values in the condition. If you supply two functions, then they will be + applied to the first and second values respectively. +2. The `In` and `NotIn` conditions accept a single mapping function, and it will be applied to all values in the + collection of values in the condition. ## Optional Conditions -All conditions support optionality - meaning they can be configured to render into the final SQL if a configured test passes. +All conditions support optionality - meaning they can be configured to render into the final SQL if a configured test +passes. Optionality is implemented via standard "filter" and "map" methods - which behave very similarly to the "filter" +and "map" methods in `java.util.Optional`. In general, if a condition's "filter" method is not satisfied, then the +condition will not be rendered. The "map" method can be used the alter the value in a condition before the condition +is rendered. For example, you could code a search like this: @@ -73,102 +129,162 @@ For example, you could code a search like this: ... SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight) .from(animalData) - .where(animalName, isEqualTo(animalName_).when(Objects::nonNull)) - .and(bodyWeight, isEqualToWhen(bodyWeight_).when(Objects::nonNull)) - .and(brainWeight, isEqualToWhen(brainWeight_).when(Objects::nonNull)) + .where(animalName, isEqualTo(animalName_).filter(Objects::nonNull)) + .and(bodyWeight, isEqualTo(bodyWeight_).filter(Objects::nonNull)) + .and(brainWeight, isEqualTo(brainWeight_).filter(Objects::nonNull)) .build() .render(RenderingStrategies.MYBATIS3); ... } ``` -In this example, the three conditions will only be rendered if the values passed to them are not null. If all three values are null, then no where clause will be generated. - -Each of the conditions accepts a lambda expression that can be used to determine if the condition should render or not. The lambdas will all be of standard JDK types (either `java.util.function.BooleanSupplier`, `java.util.function.Predicate`, or `java.util.function.BiPredicate` depending on the type of condition). The following table lists the optional conditions and shows how to use them: - -| Condition | Example | Rendering Rules | -|-----------|---------|-----------------| -| Between| where(foo, isBetween(x).and(y).when(BiPredicate)) | The library will pass x and y to the BiPredicate's test method. The condition will render if BiPredicate.test(x, y) returns true | -| Equals | where(foo, isEqualTo(x).when(Predicate)) | The library will pass x to the Predicate's test method. The condition will render if Predicate.test(x) returns true | -| Greater Than | where(id, isGreaterThan(x).when(Predicate)) | The library will pass x to the Predicate's test method. The condition will render if Predicate.test(x) returns true | -| Greater Than or Equals | where(id, isGreaterThanOrEqualTo(x).when(Predicate)) | The library will pass x to the Predicate's test method. The condition will render if Predicate.test(x) returns true | -| Less Than | where(id, isLessThan(x).when(Predicate)) | The library will pass x to the Predicate's test method. The condition will render if Predicate.test(x) returns true | -| Less Than or Equals | where(id, isLessThanOrEqualTo(x).when(Predicate)) | The library will pass x to the Predicate's test method. The condition will render if Predicate.test(x) returns true | -| Like | where(id, isLike(x).when(Predicate)) | The library will pass x to the Predicate's test method. The condition will render if Predicate.test(x) returns true | -| Like Case Insensitive | where(id, isLikeCaseInsensitive(x).when(Predicate<String>)) | The library will pass x to the Predicate's test method. The condition will render if Predicate.test(x) returns true | -| Not Between | where(id, isNotBetween(x).and(y).when(BiPredicate)) | The library will pass x and y to the BiPredicate's test method. The condition will render if BiPredicate.test(x, y) returns true | -| Not Equals | where(id, isNotEqualTo(x).when(Predicate)) | The library will pass x to the Predicate's test method. The condition will render if Predicate.test(x) returns true | -| Not Like | where(id, isNotLike(x).when(Predicate)) | The library will pass x to the Predicate's test method. The condition will render if Predicate.test(x) returns true | -| Not Like Case Insensitive | where(id, isNotLikeCaseInsensitive(x).when(Predicate<String>)) | The library will pass x to the Predicate's test method. The condition will render if Predicate.test(x) returns true | -| Not Null | where(id, isNotNull().when(BooleanSupplier) | The condition will render if BooleanSupplier.getAsBoolean() returns true | -| Null | where(id, isNull().when(BooleanSupplier) | The condition will render if BooleanSupplier.getAsBoolean() returns true | - -### "When Present" Optional Conditions -The library supplies several specializations of optional conditions to be used in the common case of checking for null values. The table below lists the rendering rules for each of these "when present" optional conditions. - -| Condition | Example | Rendering Rules | -|-----------|---------|-----------------| -| Between| where(foo, isBetweenWhenPresent(x).and(y)) | The condition will render if both x and y values are non-null | -| Equals | where(foo, isEqualToWhenPresent(x)) | The condition will render if x is non-null | -| Greater Than | where(id, isGreaterThanWhenPresent(x)) | The condition will render if x is non-null | -| Greater Than or Equals | where(id, isGreaterThanOrEqualToWhenPresent(x)) | The condition will render if x is non-null | -| Less Than | where(id, isLessThanWhenPresent(x)) | The condition will render if x is non-null | -| Less Than orEquals | where(id, isLessThanOrEqualToWhenPresent(x)) | The condition will render if x is non-null | -| Like | where(id, isLikeWhenPresent(x)) | The condition will render if x is non-null | -| Like Case Insensitive | where(id, isLikeCaseInsensitiveWhenPresent(x)) | The condition will render if x is non-null | -| Not Between | where(id, isNotBetweenWhenPresent(x).and(y)) | The condition will render if both x and y values are non-null | -| Not Equals | where(id, isNotEqualToWhenPresent(x)) | The condition will render if x is non-null | -| Not Like | where(id, isNotLikeWhenPresent(x)) | The condition will render if x is non-null | -| Not Like Case Insensitive | where(id, isNotLikeCaseInsensitiveWhenPresent(x)) | The condition will render if x is non-null | +In this example, the three conditions will only be rendered if the values passed to them are not null. +If all three values are null, then no where clause will be generated. + +Each of the conditions accepts a lambda expression that can be used to determine if the condition should render or not. +The lambdas will all be of standard JDK types (either `java.util.function.BooleanSupplier`, +`java.util.function.Predicate`, or `java.util.function.BiPredicate` depending on the type of condition). The following +table lists the optional conditions and shows how to use them: + +| Condition | Example | Rendering Rules | +|---------------------------|------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------| +| Between | where(foo, isBetween(x).and(y).filter(BiPredicate)) | The library will pass x and y to the BiPredicate's test method. The condition will render if BiPredicate.test(x, y) returns true | +| Between | where(foo, isBetween(x).and(y).filter(Predicate)) | The library will invoke the Predicate's test method twice - once with x, once with y. The condition will render if both function calls return true | +| Equals | where(foo, isEqualTo(x).filter(Predicate)) | The library will pass x to the Predicate's test method. The condition will render if Predicate.test(x) returns true | +| Greater Than | where(id, isGreaterThan(x).filter(Predicate)) | The library will pass x to the Predicate's test method. The condition will render if Predicate.test(x) returns true | +| Greater Than or Equals | where(id, isGreaterThanOrEqualTo(x).filter(Predicate)) | The library will pass x to the Predicate's test method. The condition will render if Predicate.test(x) returns true | +| Less Than | where(id, isLessThan(x).filter(Predicate)) | The library will pass x to the Predicate's test method. The condition will render if Predicate.test(x) returns true | +| Less Than or Equals | where(id, isLessThanOrEqualTo(x).filter(Predicate)) | The library will pass x to the Predicate's test method. The condition will render if Predicate.test(x) returns true | +| Like | where(id, isLike(x).filter(Predicate)) | The library will pass x to the Predicate's test method. The condition will render if Predicate.test(x) returns true | +| Like Case Insensitive | where(id, isLikeCaseInsensitive(x).filter(Predicate<String>)) | The library will pass x to the Predicate's test method. The condition will render if Predicate.test(x) returns true | +| Not Between | where(id, isNotBetween(x).and(y).filter(BiPredicate)) | The library will pass x and y to the BiPredicate's test method. The condition will render if BiPredicate.test(x, y) returns true | +| Not Between | where(foo, isNotBetween(x).and(y).filter(Predicate)) | The library will invoke the Predicate's test method twice - once with x, once with y. The condition will render if both function calls return true | +| Not Equals | where(id, isNotEqualTo(x).filter(Predicate)) | The library will pass x to the Predicate's test method. The condition will render if Predicate.test(x) returns true | +| Not Like | where(id, isNotLike(x).filter(Predicate)) | The library will pass x to the Predicate's test method. The condition will render if Predicate.test(x) returns true | +| Not Like Case Insensitive | where(id, isNotLikeCaseInsensitive(x).filter(Predicate<String>)) | The library will pass x to the Predicate's test method. The condition will render if Predicate.test(x) returns true | +| Not Null | where(id, isNotNull().filter(BooleanSupplier) | The condition will render if BooleanSupplier.getAsBoolean() returns true | +| Null | where(id, isNull().filter(BooleanSupplier) | The condition will render if BooleanSupplier.getAsBoolean() returns true | + +### "When Present" Condition Builders +The library supplies conditions for use in the common case of checking for null +values. The table below lists the rendering rules for each of these "when present" conditions. + +| Condition | Example | Rendering Rules | +|---------------------------|---------------------------------------------------|---------------------------------------------------------------| +| Between | where(foo, isBetweenWhenPresent(x).and(y)) | The condition will render if both x and y values are non-null | +| Equals | where(foo, isEqualToWhenPresent(x)) | The condition will render if x is non-null | +| Greater Than | where(id, isGreaterThanWhenPresent(x)) | The condition will render if x is non-null | +| Greater Than or Equals | where(id, isGreaterThanOrEqualToWhenPresent(x)) | The condition will render if x is non-null | +| Less Than | where(id, isLessThanWhenPresent(x)) | The condition will render if x is non-null | +| Less Than orEquals | where(id, isLessThanOrEqualToWhenPresent(x)) | The condition will render if x is non-null | +| Like | where(id, isLikeWhenPresent(x)) | The condition will render if x is non-null | +| Like Case Insensitive | where(id, isLikeCaseInsensitiveWhenPresent(x)) | The condition will render if x is non-null | +| Not Between | where(id, isNotBetweenWhenPresent(x).and(y)) | The condition will render if both x and y values are non-null | +| Not Equals | where(id, isNotEqualToWhenPresent(x)) | The condition will render if x is non-null | +| Not Like | where(id, isNotLikeWhenPresent(x)) | The condition will render if x is non-null | +| Not Like Case Insensitive | where(id, isNotLikeCaseInsensitiveWhenPresent(x)) | The condition will render if x is non-null | + +With our adoption of JSpecify, it is now considered a misuse of the library to pass a null value into a condition +unless the condition is one of the "when present" conditions. If you previously wrote code like this: + +```java +... where (id, isEqualTo(x).filter(Objects::nonNull)) ... +``` + +Starting in version 2.0.0 of the library, you will now see IDE warnings related to nullability. You should change it +to this: + +```java +... where (id, isEqualToWhenPresent(x)) ... +``` ### Optionality with the "In" Conditions -Optionality with the "in" and "not in" conditions is a bit more complex than the other types of conditions. The first thing to know is that no "in" or "not in" condition will render if the list of values is empty. For example, there will never be rendered SQL like `where name in ()`. So optionality of the "in" conditions is more about optionality of the *values* of the condition. The library comes with functions that will filter out null values, and will upper case String values to enable case insensitive queries. There are extension points to add additional filtering and mapping if you so desire. +Optionality with the "in" and "not in" conditions is a bit more complex than the other types of conditions. The rules +are different for the base conditions ("isIn", "isNotIn", etc.) and the "when present" conditions ("isInWhenPresent", +"isNotInWhenPresent", etc.). + +Optionality of the "in" conditions is more about optionality +of the *values* of the condition. The library comes with functions that will filter out null values, and will upper +case String values to enable case-insensitive queries. There are extension points to add additional filtering and +mapping if you so desire. + +Starting with version 1.5.2, we made a change to the rendering rules for the "in" conditions. This was done to limit the +danger of conditions failing to render and thus affecting more rows than expected. For the base conditions ("isIn", +"isNotIn", etc.), if the list of values is empty, then the library will throw +`org.mybatis.dynamic.sql.exception.InvalidSqlException`. We believe this is the safest outcome. For example, suppose +a DELETE statement was coded as follows: + +```java + delete.from(foo) + .where(status, isTrue()) + .and(id, isIn(Collections.emptyList())); +``` -The following table shows the different supplied In conditions and how they will render for different sets of inputs. The table assumes the following types of input: +This will cause a runtime error due to invalid SQL, but it eliminates the possibility of deleting ALL rows with +active status. If you want to allow the "in" condition to drop from the SQL if the list is empty, then use the +"inWhenPresent" condition. + +The following table shows the effect of different inputs on "in" and "inWhenPresent". The same rules apply to "notIn" +and the case-insensitive versions of these conditions: + +| Input | Effect | +|------------------------------------------|-----------------------------------------------------------------------------------| +| isIn(null) | NullPointerException thrown | +| isIn(Collections.emptyList()) | InvalidSqlException thrown | +| isIn(2, 3, null) | Rendered as "in (?, ?, ?)" (Parameter values are 2, 3, and null) | +| isInWhenPresent(null) | Condition Not Rendered | +| isInWhenPresent(Collections.emptyList()) | Condition Not Rendered | +| isInWhenPresent(2, 3, null) | Rendered as "in (?, ?)" (Parameter values are 2 and 3. The null value is dropped) | + + +The following table shows the different "in" conditions and how they will render for different sets of inputs. +The table assumes the following types of input: - Example 1 assumes an input list of ("foo", null, "bar") - like `where(name, isIn("foo", null, "bar"))` - Example 2 assumes an input list of (null) - like `where(name, isIn((String)null))` -| Condition | Nulls Filtered | Strings Mapped to Uppercase | Example 1 Rendering | Example 2 Rendering | -|-----------|----------------|--------------------|---------------------|---------------------| -| IsIn| No | No| name in ('foo', null, 'bar') | name in (null) | -| IsInWhenPresent | Yes | No | name in ('foo', 'bar') | No Render | -| IsInCaseInsensitive | No | Yes | upper(name) in ('FOO', null, 'BAR') | upper(name) in (null) | -| IsInCaseInsensiteveWhenPresent | Yes | Yes | upper(name) in ('FOO', 'BAR') | No Render | -| IsNotIn| No | No| name not in ('foo', null, 'bar') | name not in (null) | -| IsNotInWhenPresent | Yes | No | name not in ('foo', 'bar') | No render | -| IsNotInCaseInsensitive | No | Yes | upper(name) not in ('FOO', null, 'BAR') | upper(name) not in (null) | -| IsNotInCaseInsensiteveWhenPresent | Yes | Yes | upper(name) not in ('FOO', 'BAR') | No Render | +| Condition | Nulls Filtered | Strings Mapped to Uppercase | Example 1 Rendering | Example 2 Rendering | +|-----------------------------------|----------------|-----------------------------|-----------------------------------------|---------------------------| +| IsIn | No | No | name in ('foo', null, 'bar') | name in (null) | +| IsInWhenPresent | Yes | No | name in ('foo', 'bar') | Not Rendered | +| IsInCaseInsensitive | No | Yes | upper(name) in ('FOO', null, 'BAR') | upper(name) in (null) | +| IsInCaseInsensitiveWhenPresent | Yes | Yes | upper(name) in ('FOO', 'BAR') | Not Rendered | +| IsNotIn | No | No | name not in ('foo', null, 'bar') | name not in (null) | +| IsNotInWhenPresent | Yes | No | name not in ('foo', 'bar') | Not Rendered | +| IsNotInCaseInsensitive | No | Yes | upper(name) not in ('FOO', null, 'BAR') | upper(name) not in (null) | +| IsNotInCaseInsensitiveWhenPresent | Yes | Yes | upper(name) not in ('FOO', 'BAR') | Not Rendered | -If none of these options meet your needs, there is an extension point where you can add your own filter and/or map conditions to the value stream. This gives you great flexibility to alter or filter the value list before the condition is rendered. +If none of these options meet your needs, the "In" conditions also support "map" and "filter" methods for the values. +This gives you great flexibility to alter or filter the value list before the condition is rendered. -The extension point for modifying the value list is the method `then(UnaryOperator>)`. This method accepts a `UnaryOperator>` in which you can specify map and/or filter operations for the value stream. For example, suppose you wanted to code an "in" condition that accepted a list of strings, but you want to filter out any null or blank string, and you want to trim all strings. This can be accomplished with code like this: +For example, suppose you wanted to code an "in" condition that accepted a list of strings, but you want to filter out +any null or blank string, and you want to trim all strings. This can be accomplished with code like this: ```java - SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight) - .from(animalData) - .where(animalName, isIn(" Mouse", " ", null, "", "Musk shrew ") - .then(s -> s.filter(Objects::nonNull) - .map(String::trim) - .filter(st -> !st.isEmpty()))) - .orderBy(id) - .build() - .render(RenderingStrategies.MYBATIS3); + SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight) + .from(animalData) + .where(animalName, isIn(" Mouse", " ", null, "", "Musk shrew ") + .filter(Objects::nonNull) + .map(String::trim) + .filter(not(String::isEmpty))) + .orderBy(id) + .build() + .render(RenderingStrategies.MYBATIS3); ``` -This code is a bit cumbersome, so if this is a common use case you could write a specialization of the `IsIn` condition as follows: +This code is a bit cumbersome, so if this is a common use case you could build a specialization of the `IsIn` condition +as follows: ```java -public class MyInCondition extends IsIn { - protected MyInCondition(List values) { - super(values, s -> s.filter(Objects::nonNull) - .map(String::trim) - .filter(st -> !st.isEmpty())); - } - - public static MyInCondition isIn(String...values) { - return new MyInCondition(Arrays.asList(values)); +import org.mybatis.dynamic.sql.SqlBuilder; + +public class MyInCondition { + public static IsIn isIn(String... values) { + return SqlBuilder.isIn(values) + .filter(Objects::nonNull) + .map(String::trim) + .filter(not(String::isEmpty)); } } ``` @@ -176,12 +292,28 @@ public class MyInCondition extends IsIn { Then the condition could be used in a query as follows: ```java - SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight) - .from(animalData) - .where(animalName, MyInCondition.isIn(" Mouse", " ", null, "", "Musk shrew ")) - .orderBy(id) - .build() - .render(RenderingStrategies.MYBATIS3); + SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight) + .from(animalData) + .where(animalName, MyInCondition.isIn(" Mouse", " ", null, "", "Musk shrew ")) + .orderBy(id) + .build() + .render(RenderingStrategies.MYBATIS3); ``` -You can apply value stream operations to the conditions `IsIn`, `IsInCaseInsensitive`, `IsNotIn`, and `IsNotInCaseInsensitive`. With the case insensitive conditions, the library will automatically convert non-null strings to upper case after any value stream operation you specify. +## Potential for Non Rendering Where Clauses + +An "inWhenPresent" condition will be dropped from rendering if the list of values is empty. Other conditions could be +dropped from a where clause due to filtering. If all conditions fail to render, then the entire where clause will be +dropped from the rendered SQL. In general, we think it is a good thing that the library will not render invalid SQL. +But this stance does present a danger - if a where clause is dropped from the rendered SQL, then the statement could +end up impacting all rows in a table. For example, a delete statement could inadvertently delete all rows in a table. + +By default, and out of an abundance of caution, the library will not allow a statement to render if the entire where +clause will be dropped. If a where clause is coded, but fails to render, then the library will throw a +`NonRenderingWhereClauseException` by default. + +If no where clause is coded in a statement, then we assume the statement is intended to affect all rows in a table. +In that case no exception will be thrown. + +This behavior can be modified through configuration - either globally for the entire library, or for each statement +individually. See the "Configuration of the Library" page for details on configuration options. diff --git a/src/site/markdown/docs/configuration.md b/src/site/markdown/docs/configuration.md new file mode 100644 index 000000000..4c228a55d --- /dev/null +++ b/src/site/markdown/docs/configuration.md @@ -0,0 +1,81 @@ +# Configuration of the Library + +This page will detail the behaviors of MyBatis Dynamic SQL that can be modified. +Configuration is available with version 1.4.1 and later of the library. + +The library can be configured globally - which will change the behavior for all statements - or each individual statement +can be configured. There are sensible defaults for all configuration values, so configuration is not strictly necessary. +If you want to change any of the default behaviors of the library, then the information on this page will help. + +## Global Configuration + +On first use the library will initialize the global configuration. The global configuration can be specified via a property +file named `mybatis-dynamic-sql.properties` in the root of the classpath. If you wish to use a different file name, +you can specify the file name as a JVM property named `mybatis-dynamic-sql.configurationFile`. Note that the global +configuration is created one time and shared for every statement in the same JVM. + +The configuration file is a standard Java properties file. The possible values are detailed in the next section. + +## Global Configuration Properties + +| Property | Default | Available in Version | Meaning | +|------------------------------------|---------|----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| nonRenderingWhereClauseAllowed | false | 1.4.1+ | If a where clause is specified, but fails to render, then the library will throw a `NonRenderingWhereClauseException` by default. If you set this value to true, then no exception will be thrown. This could enable statements to be rendered without where clauses that affect all rows in a table. | + +## Statement Configuration + +If the global configuration is not acceptable for any individual statement, you can also configure the statement in the +DSL. Consider the following statement: + +```java +DeleteStatementProvider deleteStatement = deleteFrom(animalData) + .where(id, isIn(null, 22, null).filter(Objects::nonNull).filter(i -> i != 22)) + .configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true)) + .build() + .render(RenderingStrategies.MYBATIS3); +``` + +In this case, the `isIn` condition has filters that will remove all values from the condition. In that case, the +condition will not render and, subsequently, the where clause will not render. This means that the generated delete +statement would delete all rows in the table. By default, the global configuration will block this statement from +rendering and throw a `NonRenderingWhereClauseException`. If for some reason you would like to allow a statement +like this to be rendered, then you can allow it as shown above with the `configureStatement` method. + +The Kotlin DSL contains the same function: + +```kotlin +val deleteStatement = deleteFrom(person) { + where { id isEqualToWhenPresent null } + configureStatement { isNonRenderingWhereClauseAllowed = true } +} +``` + +## Configuration Scope with Select Statements + +Select statements can stand alone, or they can be embedded within other statements. For example, the library supports +writing insert statements with an embedded select, or select statements that contain other select statements for sub +queries. The select DSLs (both Java and Kotlin) appear to allow you to specify statement configuration on embedded +select statements, but this is not supported in point of fact. Statement configuration must ALWAYS be specified on the +outermost statement. Any configuration specified on embedded select statements will be ignored. We realize this could be +confusing! But we've made this decision hoping to minimize code duplication and maximize consistency. + +So the best practice is to ALWAYS specify the statement configuration as the LAST call to the DSL before calling +`build`, or before ending a Kotlin lambda. + +The following Kotlin code snippet shows this in action... + +```kotlin +val insertStatement = insertSelect { + into(person) + select(id, firstName, lastName, birthDate, employed, occupation, addressId) { + from(person) + where { id isGreaterThanOrEqualToWhenPresent null } + // the following will be ignored in favor of the enclosing statement configuration... + configureStatement { isNonRenderingWhereClauseAllowed = false } + } + configureStatement { isNonRenderingWhereClauseAllowed = true } +} +``` + +The inner `configureStatement` call will be ignored in this case, only the `configureStatement` call scoped to the +insert statement itself will be in effect. diff --git a/src/site/markdown/docs/databaseObjects.md b/src/site/markdown/docs/databaseObjects.md index 2b69bdf94..3789ec890 100644 --- a/src/site/markdown/docs/databaseObjects.md +++ b/src/site/markdown/docs/databaseObjects.md @@ -3,34 +3,41 @@ MyBatis Dynamic SQL works with Java objects that represent relational tables or ## Table or View Representation -The class `org.mybatis.dynamic.sql.SqlTable` is used to represent a table or view in a database. An `SqlTable` holds a name, and a collection of `SqlColumn` objects that represent the columns in a table or view. +The class `org.mybatis.dynamic.sql.SqlTable` is used to represent a table or view in a database. An `SqlTable` holds a +name, and a collection of `SqlColumn` objects that represent the columns in a table or view. A subclass of `SqlTable` - +`AliasableSqlTable` should be used in cases where you want to specify a table alias that should be used in all cases, +or if you need to change the table name at runtime. A table or view name in SQL has three parts: -1. The catalog - which is optional and is rarely used outside of Microsoft SQL Server. If unspecified the default catalog will be used - and many databases only have one catalog -1. The schema - which is optional but is very often specified. If unspecified the default schema will be used -1. The table name - which is required +1. The catalog - which is optional and is rarely used outside of Microsoft SQL Server. If unspecified the default + catalog will be used - and many databases only have one catalog +2. The schema - which is optional but is very often specified. If unspecified, the default schema will be used +3. The table name - which is required Typical examples of names are as follows: - `"dbo..bar"` - a name with a catalog (dbo) and a table name (bar). This is typical for SQL Server -- `"foo.bar"` - a name with a schema (foo) and a table name (bar). This is typical in many databases when you want to access tables that are not in the default schema -- `"bar"` - a name with just a table name (bar). This will access a table or view in the default catalog and schema for a connection +- `"foo.bar"` - a name with a schema (foo) and a table name (bar). This is typical in many databases when you want to + access tables that are not in the default schema +- `"bar"` - a name with just a table name (bar). This will access a table or view in the default catalog and schema for + a connection -In MyBatis Dynamic SQL, the table or view name can be specified in different ways: +In MyBatis Dynamic SQL, the full name of the table should be supplied on the constructor of the table object. +If a table name needs to change at runtime (say for sharding support), then use the `withName` method on +`AliasableSqlTable` to create an instance with the new name. -1. The name can be a constant String -1. The name can be calculated at runtime based on a catalog and/or schema supplier functions and a constant table name -1. The name can be calculated at runtime with a name supplier function +We recommend using the base class `AliasableSqlTable` in all cases as it provides the most flexibility. The +`SqlTable` class remains in the library for compatibility with older code only. -### Constant Names - -Constant names are used when you use the `SqlTable` constructor with a single String argument. For example: +For example: ```java -public class MyTable extends SqlTable { +import org.mybatis.dynamic.sql.AliasableSqlTable; + +public class MyTable extends AliasableSqlTable { public MyTable() { - super("MyTable"); + super("MyTable", MyTable::new); } } ``` @@ -38,107 +45,140 @@ public class MyTable extends SqlTable { Or ```java -public class MyTable extends SqlTable { +public class MyTable extends AliasableSqlTable { public MyTable() { - super("MySchema.MyTable"); + super("MySchema.MyTable", MyTable::new); } } ``` -### Dynamic Catalog and/or Schema Names -MyBatis Dynamic SQL allows you to dynamically specify a catalog and/or schema. This is useful for applications where the schema may change for different users or environments, or if you are using different schemas to shard the database. Dynamic names are used when you use a `SqlTable` constructor that accepts one or more `java.util.function.Supplier` arguments. - -For example, suppose you wanted to change the schema based on the value of a system property. You could write a class like this: +You can change a table name: ```java -public class SchemaSupplier { - public static final String schema_property = "schemaToUse"; - - public static Optional schemaPropertyReader() { - return Optional.ofNullable(System.getProperty(schema_property)); +public class MyTable extends AliasableSqlTable { + public MyTable() { + super("Schema1.MyTable", MyTable::new); } } + +MyTable schema1Table = new MyTable(); +MyTable schema2Table = schema1Table.withName("Schema2.MyTable"); ``` -This class has a static method `schemaPropertyReader` that will return an `Optional` containing the value of a system property. You could then reference this method in the constructor of the `SqlTable` like this: +## Aliased Tables + +In join queries, it is usually a good practice to specify table aliases. The `select` statement includes +support for specifying table aliases in each query in a way that looks like natural SQL. For example: ```java -public static final class User extends SqlTable { - public User() { - super(SchemaSupplier::schemaPropertyReader, "User"); - } -} + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderLine.lineNumber, itemMaster.description, orderLine.quantity) + .from(orderMaster, "om") + .join(orderLine, "ol").on(orderMaster.orderId, equalTo(orderLine.orderId)) + .join(itemMaster, "im").on(orderLine.itemId, equalTo(itemMaster.itemId)) + .where(orderMaster.orderId, isEqualTo(2)) + .build() + .render(RenderingStrategies.MYBATIS3); ``` -Whenever the table is referenced for rendering SQL, the name will be calculated based on the current value of the system property. +In a query like this, the library will automatically append the table alias to the column name when the query is rendered. +Internally, the alias for a column is determined by looking up the associated table in a HashMap maintained within the +query model. If you do not specify a table alias, the library will automatically append the table name in join queries. -There are two constructors that can be used for dynamic names: - -1. A constructor that accepts `Supplier>` for the schema, and `String` for the name. This constructor assumes that the catalog is always empty or not used -1. A constructor that accepts `Supplier>` for the catalog, `Supplier>` for the schema, and `String` for the name - -If you are using Microsoft SQL Server and want to use a dynamic catalog name and ignore the schema, then you should use the second constructor like this: +Unfortunately, this strategy fails for self-joins. It can also get confusing when there are sub-queries. Imagine a +query like this: ```java -public static final class User extends SqlTable { - public User() { - super(CatalogSupplier::catalogPropertyReader, Optional::empty, "User"); - } -} + SelectStatementProvider selectStatement = select(user.userId, user.userName, user.parentId) + .from(user, "u1") + .join(user, "u2").on(user.userId, equalTo(user.parentId)) + .where(user.userId, isEqualTo(4)) + .build() + .render(RenderingStrategies.MYBATIS3); ``` -The following table shows how the name is calculated in all combinations of suppliers: - -Catalog Supplier Value | Schema Supplier Value | Name | Fully Qualified Name ----|---|---|--- -"MyCatalog" | "MySchema" | "MyTable" | "MyCatalog.MySchema.MyTable" -<empty> | "MySchema" | "MyTable" | "MySchema.MyTable" -"MyCatalog" | <empty> | "MyTable" | "MyCatalog..MyTable" -<empty> | <empty> | "MyTable" | "MyTable" - +In this query it is not clear which instance of the `user` table is used for each column, and there will only be entry in the +HashMap for the `user` table - so only one of the aliases specified in the select statement will be in effect. +There are two ways to deal with this problem. -### Fully Dynamic Names -MyBatis Dynamic SQL allows you to dynamically specify a full table name. This is useful for applications where the database is sharded with different tables representing different shards of the whole. Dynamic names are used when you use a `SqlTable` constructor that accepts a single `java.util.function.Supplier` argument. +The first is to simply create another instance of the User `SqlTable` object. With this method it is very clear which column +belongs to which instance of the table and the library can easily calculate aliases: -Note that this functionality should only be used for tables that have different names, but are otherwise identical. +```java + User user1 = new User(); + User user2 = new User(); + SelectStatementProvider selectStatement = select(user1.userId, user1.userName, user1.parentId) + .from(user1, "u1") + .join(user2, "u2").on(user1.userId, equalTo(user2.parentId)) + .where(user2.userId, isEqualTo(4)) + .build() + .render(RenderingStrategies.MYBATIS3); +``` -For example, suppose you wanted to change the name based on the value of a system property. You could write a class like this: +Starting with version 1.3.1, there is new method where the alias can be specified in the table object itself. This allows +you to move the aliases out of the `select` statement. ```java -public class NameSupplier { - public static final String name_property = "nameToUse"; - - public static String namePropertyReader() { - return System.getProperty(name_property); - } -} + User user1 = user.withAlias("u1"); + User user2 = user.withAlias("u2"); + + SelectStatementProvider selectStatement = select(user1.userId, user1.userName, user1.parentId) + .from(user1) + .join(user2).on(user1.userId, equalTo(user2.parentId)) + .where(user2.userId, isEqualTo(4)) + .build() + .render(RenderingStrategies.MYBATIS3); ``` -This class has a static method `namePropertyReader` that will return an `String` containing the value of a system property. You could then reference this method in the constructor of the `SqlTable` like this: +To enable this support, your table objects should extend `org.mybatis.dynamic.sql.AliasableSqlTable` rather than +`org.mybatis.dynamic.sql.SqlTable` as follows: ```java -public static final class User extends SqlTable { - public User() { - super(NameSupplier::namePropertyReader); + public static final class User extends AliasableSqlTable { + public final SqlColumn userId = column("user_id", JDBCType.INTEGER); + public final SqlColumn userName = column("user_name", JDBCType.VARCHAR); + public final SqlColumn parentId = column("parent_id", JDBCType.INTEGER); + + public User() { + super("User", User::new); + } } -} ``` -Whenever the table is referenced for rendering SQL, the name will be calculated based on the current value of the system property. +If you use an aliased table object, and also specify an alias in the `select` statement, the alias from the `select` +statement will override the alias in the table object. +## Column Representation +The class `org.mybatis.dynamic.sql.SqlColumn` is used to represent a column in a table or view. An `SqlColumn` is always +associated with a `SqlTable`. In its most basic form, the `SqlColumn` class holds a name and a reference to the +`SqlTable` it is associated with. The table reference is required so that table aliases can be applied to columns in the +rendering phase. -## Column Representation +The `SqlColumn` will be rendered in SQL based on the `RenderingStrategy` applied to the SQL statement. Typically the +rendering strategy generates a string that represents a parameter marker in whatever SQL engine you are using. For +example, MyBatis3 parameter markers are formatted as "#{some_attribute}". By default, all columns are rendered with the +same strategy. The library supplies rendering strategies that are appropriate for several SQL execution engines +including MyBatis3 and Spring JDBC template. -The class `org.mybatis.dynamic.sql.SqlColumn` is used to represent a column in a table or view. An `SqlColumn` is always associated with a `SqlTable`. In it's most basic form, the `SqlColumn` class holds a name and a reference to the `SqlTable` it is associated with. +In some cases it is necessary to override the rendering strategy for a particular column - so the `SqlColumn` class +supports specifying a rendering strategy for a column that will override the rendering strategy applied to a statement. +A good example of this use case is with PostgreSQL. In that database it is required to add the string "::jsonb" to a +prepared statement parameter marker when inserting or updating JSON fields, but not for other fields. A column based +rendering strategy enables this. -The `SqlColumn` class has additional optional attributes that are useful for SQL rendering - especially in MyBatis3. These include: +The `SqlColumn` class has additional optional attributes that are useful for SQL rendering - especially in MyBatis3. +These include: -* The `java.sql.JDBCType` of the column. This will be rendered into the MyBatis3 compatible parameter marker - which helps with picking type handlers and also inserting or updating null capable fields -* A String containing a type handler - either a type handler alias or the fully qualified type of a type handler. This will be rendered into the MyBatis3 compatible parameter marker +* The `java.sql.JDBCType` of the column. This will be rendered into the MyBatis3 compatible parameter marker - which + helps with picking type handlers and also inserting or updating null capable fields +* A String containing a type handler - either a type handler alias or the fully qualified type of a type handler. This + will be rendered into the MyBatis3 compatible parameter marker -If you are not using MyBatis3, then you will not need to specify the JDBC Type or type handler. +If you are not using MyBatis3, then you do not need to specify the JDBC Type or type handler as those attributes are +ignored by other rendering strategies. -Finally, the `SqlColumn` class has methods to designate a column alias or sort order for use in different SQL statements. +Finally, the `SqlColumn` class has methods to designate a column alias or sort order for use in different SQL +statements. -We recommend a usage pattern for creating table and column objects that provides quite a bit of flexibility for usage. See the [Quick Start](quickStart.html) page for a complete example. +We recommend a usage pattern for creating table and column objects that provides quite a bit of flexibility for usage. +See the [Quick Start](quickStart.html) page for a complete example. diff --git a/src/site/markdown/docs/delete.md b/src/site/markdown/docs/delete.md index 6c421c5e5..5de5bf42b 100644 --- a/src/site/markdown/docs/delete.md +++ b/src/site/markdown/docs/delete.md @@ -16,13 +16,13 @@ For example: DeleteStatementProvider deleteStatement = deleteFrom(foo) .build() .render(RenderingStrategies.MYBATIS3); -``` +``` ## Annotated Mapper for Delete Statements The DeleteStatementProvider object can be used as a parameter to a MyBatis mapper method directly. If you are using an annotated mapper, the delete method should look like this: - + ```java import org.apache.ibatis.annotations.DeleteProvider; import org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider; @@ -37,10 +37,11 @@ import org.mybatis.dynamic.sql.util.SqlProviderAdapter; ## XML Mapper for Delete Statements -We do not recommend using an XML mapper for delete statements, but if you want to do so the DeleteStatementProvider object can be used as a parameter to a MyBatis mapper method directly. +We do not recommend using an XML mapper for delete statements, but if you want to do so, the DeleteStatementProvider +object can be used as a parameter to a MyBatis mapper method directly. If you are using an XML mapper, the delete method should look like this in the Java interface: - + ```java import org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider; diff --git a/src/site/markdown/docs/exceptions.md b/src/site/markdown/docs/exceptions.md new file mode 100644 index 000000000..8cf4263b3 --- /dev/null +++ b/src/site/markdown/docs/exceptions.md @@ -0,0 +1,53 @@ +# Exceptions Thrown by the Library + +The library will throw runtime exceptions in a variety of cases - most often when invalid SQL is detected. + +All exceptions are derived from `org.mybatis.dynamic.sql.exception.DynamicSqlException` which is, in turn, +derived from `java.lang.RuntimeException`. + +The most important exceptions to think about are `InvalidSQLException` and `NonRenderingWhereClauseException`. We +provide details about those exceptions below. + +## Invalid SQL Detection + +The library makes an effort to prevent the generation of invalid SQL. If invalid SQL is detected, the library will +throw `InvalidSQLException` or one of it's derived exceptions. Invalid SQL can happen in different ways: + +1. Misuse of the DSL. For example, the DSL allows you to build an update statement with no "set" clauses. + Even though technically allowed by the DSL, this would produce invalid SQL. +2. Misuse of the Kotlin DSL. The Kotlin DSL provides a lot of flexibility for building statements and looks very close + to native SQL, but that flexibility can be misused. For example, the Kotlin DSL would allow you to write an insert + statement without an "into" clause. +3. More common is a case when using the optional mappings in an insert or update statement. It is possible + that all mappings would fail to render which would produce invalid SQL. For example, in a general insert statement + you could specify many "set column to value when present" mappings that all had null values. All mappings would fail + to render in that case which would cause invalid SQL. + +All of these exceptions can be avoided through proper use of the DSL and validation of input values. + +## Non Rendering Where Clauses + +Most conditions in a where clause provide optionality - they have `filter` methods that can cause the condition to be +dropped from the where clause. If all the conditions in a where clause fail to render, then the where clause itself is +dropped from the rendered SQL. This can be dangerous in that it can cause a statement to be generated that affects all +rows in a table. For example, all rows could be deleted. As of version 1.4.1, the library will throw a +`NonRenderingWhereClauseException` in this case out of an abundance of caution. This behavior can be overridden +through either global configuration, or by configuring individual statements to allow for where clauses to be dropped. + +The important idea is that there are legitimate cases when it is reasonable to allow a where clause to not render, but +the decision to allow that should be very intentional. See the [Configuration of the Library](configuration.md) page for further details. + +The exception will only be thrown if a where clause is coded but fails to render. If you do not code a where clause in +a statement, then we assume that you intend for all rows to be affected. + +## Exception Details + +Details of the different exceptions follows: + +| Exception | Causes | +|----------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `org.mybatis.dynamic.sql.exception.DuplicateTableAliasException` | Thrown if you attempt to join more than one table with the same alias in a select statement | +| `org.mybatis.dynamic.sql.exception.DynamicSQLException` | Thrown when other more specific exceptions are not appropriate. One example is when reading a configuration property file causes an IOException. This is a rare occurrence. | +| `org.mybatis.dynamic.sql.exception.InvalidSQLException` | Thrown if invalid SQL is detected. The most common causes are when all the optional column mappings in an insert or update statement fail to render. | +| `org.mybatis.dynamic.sql.exception.NonRenderingWhereClauseException` | Thrown if all conditions in a where clause fail to render - which will cause the where clause to be dropped from the rendered SQL. This could cause a statement to inadvertently affect all rows in a table. This behavior can be changed with global or statement configuration. | +| `org.mybatis.dynamic.sql.util.kotlin.KInvalidSqlException` | Thrown if invalid SQL is detected when using the Kotlin DSL. This exception is for specific misuses of the Kotlin DSL. It is derived from `InvalidSQLException` which can also occur when using the Kotlin DSL. | diff --git a/src/site/markdown/docs/extending.md b/src/site/markdown/docs/extending.md index a20560cf3..a1fee9adb 100644 --- a/src/site/markdown/docs/extending.md +++ b/src/site/markdown/docs/extending.md @@ -1,46 +1,59 @@ # Extending MyBatis Dynamic SQL -The library has been designed for extension from the very start of the design. We do not believe that the library covers all possible uses and we wanted to make it possible to add functionality that suits the needs of different projects. +The library has been designed for extension from the very beginning. We do not believe that the library +covers all possible uses, and we wanted to make it possible to add functionality that suits the needs of different +projects. This page details the main extension points of the library. ## Extending the SELECT Capabilities -The SELECT support is the most complex part of the library, and also the part of the library that is most likely to be extended. There are two main interfaces involved with extending the SELECT support. Picking which interface to implement is dependent on how you want to use your extension. - -| Interface | Purpose| -|-----------|--------| -| `org.mybatis.dynamic.sql.BasicColumn` | Use this interface if you want to add capabilities to a SELECT list or a GROUP BY expression. For example, creating a calculated column. | -| `org.mybatis.dynamic.sql.BindableColumn` | Use this interface if you want to add capabilities to a WHERE clause. For example, creating a custom condition. | +The SELECT support is the most complex part of the library, and also the part of the library that is most likely to be +extended. There are two main interfaces involved with extending the SELECT support. Picking which interface to +implement is dependent on how you want to use your extension. + +| Interface | Purpose | +|------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `org.mybatis.dynamic.sql.BasicColumn` | Use this interface if you want to add capabilities to a SELECT list, a GROUP BY, or an ORDER BY expression. For example, using a database function. | +| `org.mybatis.dynamic.sql.BindableColumn` | Use this interface if you want to add capabilities to a WHERE clause in addition to the capabilities of `BasicColumn`. For example, creating a custom condition. | + +Rendering is the process of generating an appropriate SQL fragment to implement the function or calculated column. +The library will call a method `render(RenderingContext)` in your implementation. This method should return an +instance of `FragmentAndParameters` containing your desired SQL fragment and any bind parameters needed. Bind +parameter markers can be calculated by calling the `RenderingContext.calculateParameterInfo()` method. That method will +return a properly formatted bind marker for the SQL string, and a matching Map key you should use in your parameter map. +In general, you do not need to worry about adding spacing, commas, etc. before or after your fragment - the library +will properly format the final statement from all the different fragments. See the following sections for examples. ### Supporting Calculated Columns -A calculated column can be used anywhere in a SELECT statement. If you don't need to use it in a WHERE clause, then it is easier to implement the `org.mybatis.dynamic.sql.BasicColumn` interface. An example from the library itself is the `org.mybatis.dynamic.sql.select.aggregate.CountAll` class: +A calculated column can be used anywhere in a SELECT statement. If you don't need to use it in a WHERE clause, then it +is easier to implement the `org.mybatis.dynamic.sql.BasicColumn` interface. An example follows: ```java +import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + public class CountAll implements BasicColumn { - - private Optional alias = Optional.empty(); - public CountAll() { - super(); - } + private String alias; @Override - public String renderWithTableAlias(TableAliasCalculator tableAliasCalculator) { - return "count(*)"; //$NON-NLS-1$ + public FragmentAndParameters render(RenderingContext renderingContext) { + return FragmentAndParameters.fromFragment("count(*)"); //$NON-NLS-1$ } @Override public Optional alias() { - return alias; + return Optional.ofNullable(alias); } @Override public CountAll as(String alias) { CountAll copy = new CountAll(); - copy.alias = Optional.of(alias); + copy.alias = alias; return copy; } } @@ -48,77 +61,200 @@ public class CountAll implements BasicColumn { This class is used to implement the `count(*)` function in a SELECT list. There are only three methods to implement: -1. `renderWithTableAlias` - the default renderers will write the value returned from this function into the select list - or the GROUP BY expression. If your item can be altered by a table alias, then here is where you change the return value based on the alias specified by the user. For a `count(*)` expression, a table alias never applies, so we just return the same value whether or not an alias has been specified by the user. -2. `as` - this method can be called by the user to add an alias to the column expression. In the method you should return a new instance of the object, with the alias passed by the user. -3. `alias` - this method is called by the default renderer to obtain the column alias for the select list. If there is no alias, then returning Optional.empty() will disable setting a column alias. +1. `render` - the default renderers will write the value returned from this function into the select list - or the + GROUP BY expression. If your item can be altered by a table alias, then here is where you change the return value + based on the alias specified by the user. For a `count(*)` expression, a table alias never applies, so we just + return the same value regardless of whether an alias has been specified by the user. +2. `as` - this method can be called by the user to add an alias to the column expression. In the method you should + return a new instance of the object, with the alias passed by the user. +3. `alias` - this method is called by the default renderer to obtain the column alias for the select list. If there is + no alias, then returning Optional.empty() will disable setting a column alias. + +## Writing Custom Functions + +Relational database vendors provide hundreds of functions in their SQL dialects to aid with queries and offload +processing to the database servers. This library does not try to implement every function that exists. This library +also does not provide any abstraction over the different functions on different databases. For example, bitwise operator +support is non-standard, and it would be difficult to provide a function in this library that worked on every database. +So we take the approach of supplying examples for a few very common functions, and making it relatively easy to write +your own functions. + +The supplied functions are all in the `org.mybatis.dynamic.sql.select.function` package. They are all implemented +as `BindableColumn` - meaning they can appear in a select list or a where clause. -### Writing a Custom Where Condition +We provide some base classes that you can easily extend to write functions of your own. Those classes are as follows: -If you want to use your calculated column in a WHERE clause in addition the select list and the GROUP BY clause, then you must implement `org.mybatis.dynamic.sql.BindableColumn`. This interface extends the `BasicColumn` interface from above and adds two methods. An example from the library is the `org.mybatis.dynamic.sql.select.function.Add` class: +Note: the base classes are all in the `org.mybatis.dynamic.sql.select.function` package. + +| Interface | Purpose | +|----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `o.m.d.s.s.f.AbstractTypeConvertingFunction` | Extend this class if you want to build a function that changes a column data type. For example, using a database function to calculate the Base64 String for a binary field. | +| `o.m.d.s.s.f.AbstractUniTypeFunction` | Extend this class if you want to build a function that does not change a column data type. For example UPPER(), LOWER(), etc. | +| `o.m.d.s.s.f.OperatorFunction` | Extend this class if you want to build a function the implements an operator. For example column1 + column2. | + +### AbstractTypeConvertingFunction Example + +The following function uses HSQLDB's `TO_BASE64` function to calculate the BASE64 string for a binary field. Note that +the function changes the data type from `byte[]` to `String`. ```java -public class Add implements BindableColumn { - - private Optional alias = Optional.empty(); - private BindableColumn column1; - private BindableColumn column2; - - private Add(BindableColumn column1, BindableColumn column2) { - this.column1 = Objects.requireNonNull(column1); - this.column2 = Objects.requireNonNull(column2); - } +import java.sql.JDBCType; +import java.util.Optional; - @Override - public Optional alias() { - return alias; - } +import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.select.function.AbstractTypeConvertingFunction; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + +public class ToBase64 extends AbstractTypeConvertingFunction { + + private ToBase64(BasicColumn column) { + super(column); + } + + @Override + public Optional jdbcType() { + return Optional.of(JDBCType.VARCHAR); + } + + @Override + public FragmentAndParameters render(RenderingContext renderingContext) { + FragmentAndParameters renderedColumn = column.render(renderingContext); + + return FragmentAndParameters + .withFragment("TO_BASE64(" + renderedColumn.fragment() + ")") //$NON-NLS-1$ //$NON-NLS-2$ + .withParameters(renderedColumn.parameters()) + .build(); + } + + @Override + protected ToBase64 copy() { + return new ToBase64(column); + } + + public static ToBase64 toBase64(BindableColumn column) { + return new ToBase64(column); + } +} +``` - @Override - public String renderWithTableAlias(TableAliasCalculator tableAliasCalculator) { - return column1.applyTableAliasToName(tableAliasCalculator) - + " + " //$NON-NLS-1$ - + column2.applyTableAliasToName(tableAliasCalculator); - } +### AbstractUniTypeFunction Example - @Override - public BindableColumn as(String alias) { - Add newColumn = new Add<>(column1, column2); - newColumn.alias = Optional.of(alias); - return newColumn; - } +The following function implements the common database `UPPER()` function. - @Override - public JDBCType jdbcType() { - return column1.jdbcType(); - } +```java +import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.select.function.AbstractUniTypeFunction; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + +public class Upper extends AbstractUniTypeFunction { + + private Upper(BasicColumn column) { + super(column); + } + + @Override + public FragmentAndParameters render(RenderingContext renderingContext) { + FragmentAndParameters renderedColumn = column.render(renderingContext); + + return FragmentAndParameters + .withFragment("upper(" + renderedColumn.fragment() + ")") //$NON-NLS-1$ //$NON-NLS-2$ + .withParameters(renderedColumn.parameters()) + .build(); + } + + @Override + protected Upper copy() { + return new Upper(column); + } + + public static Upper of(BindableColumn column) { + return new Upper(column); + } +} +``` - @Override - public Optional typeHandler() { - return column1.typeHandler(); - } +Note that `FragmentAndParameters` has a utility method that can simplify the implementation if you do not need to +add any new parameters to the resulting fragment. For example, the UPPER function can be simplified as follows: - public static Add of(BindableColumn column1, BindableColumn column2) { - return new Add<>(column1, column2); - } +```java +import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.select.function.AbstractUniTypeFunction; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + +public class Upper extends AbstractUniTypeFunction { + + private Upper(BasicColumn column) { + super(column); + } + + @Override + public FragmentAndParameters render(RenderingContext renderingContext) { + return column.render(renderingContext).mapFragment(f -> "upper(" + f + ")"); //$NON-NLS-1$ //$NON-NLS-2$ + } + + @Override + protected Upper copy() { + return new Upper(column); + } + + public static Upper of(BindableColumn column) { + return new Upper(column); + } } ``` -This class implements the idea of adding two numeric columns together in a SELECT statement. This class accepts two other columns as parameters and then overrides `renderWithTableAlias` to render the addition. The `alias` and `as` methods work as described above. -The two additional methods are: +### OperatorFunction Example -1. `jdbcType` - returns the JDBC Type of the column for the WHERE clause. This is used by the MyBatis3 rendering strategy to render a full MyBatis parameter expression. -2. `typeHandler` - returns a type handler if specified by the user. Again, this is used by the MyBatis3 rendering strategy to render a full MyBatis parameter expression. +The following function implements the concatenate operator. Note that the operator can be applied to list of columns of +arbitrary length: +```java +import java.util.Arrays; +import java.util.List; + +import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.select.function.OperatorFunction; + +public class Concatenate extends OperatorFunction { + + protected Concatenate(BasicColumn firstColumn, BasicColumn secondColumn, + List subsequentColumns) { + super("||", firstColumn, secondColumn, subsequentColumns); //$NON-NLS-1$ + } + + @Override + protected Concatenate copy() { + return new Concatenate<>(column, secondColumn, subsequentColumns); + } + + public static Concatenate concatenate(BindableColumn firstColumn, BasicColumn secondColumn, + BasicColumn... subsequentColumns) { + return new Concatenate<>(firstColumn, secondColumn, Arrays.asList(subsequentColumns)); + } +} +``` ## Writing Custom Rendering Strategies -A RenderingStrategy is used to format the parameter placeholders in generated SQL. The library ships with two built-in rendering strategies: +A RenderingStrategy is used to format the parameter placeholders in generated SQL. The library ships with two built-in +rendering strategies: -1. A strategy that is suitable for MyBatis3. This strategy generates placeholders in the format required by MyBatis3 (for example `#{foo,jdbcType=INTEGER}`). -2. A strategy that is suitable for the Spring NamedParameterJDBCTemplate. This strategy generates placeholders in the format required by Spring (for example `:foo`). +1. A strategy that is suitable for MyBatis3. This strategy generates placeholders in the format required by MyBatis3 + (for example `#{foo,jdbcType=INTEGER}`). +2. A strategy that is suitable for the Spring NamedParameterJDBCTemplate. This strategy generates placeholders in the + format required by Spring (for example `:foo`). -You can write a custom rendering strategy if you want to use the library with some other framework. For example, if you wanted to use the library to generate SQL that could be prepared directly by JDBC, you could write a rendering strategy that simply uses the question mark (`?`) for all parameters. +You can write a custom rendering strategy if you want to use the library with some other framework. For example, if you +wanted to use the library to generate SQL that could be prepared directly by JDBC, you could write a rendering strategy +that simply uses the question mark (`?`) for all parameters. ```java import org.mybatis.dynamic.sql.BindableColumn; @@ -140,11 +276,105 @@ public class PlainJDBCRenderingStrategy extends RenderingStrategy { The library will pass the following parameters to the `getFormattedJdbcPlaceholder` method: 1. `column` - the column definition for the current placeholder -2. `prefix` - For INSERT statements the value will be "record", for all other statements (including inserts with selects) the value will be "parameters" -3. `parameterName` - this will be the unique name for the the parameter. For INSERT statements, the name will be the property of the inserted record that is mapped to this parameter. For all other statements (including inserts with selects) a unique name will be generated by the library. That unique name will also be used to place the value of the parameter into the parameters Map. +2. `prefix` - For INSERT statements the value will be "record", for all other statements (including inserts with + selects) the value will be "parameters" +3. `parameterName` - this will be the unique name for the parameter. For INSERT statements, the name will be the + property of the inserted record mapped to this parameter. For all other statements (including inserts with selects) + a unique name will be generated by the library. That unique name will also be used to place the value of the + parameter into the parameters Map. ## Writing Custom Renderers -SQL rendering is accomplished by classes that are decoupled from the SQL model classes. All the model classes have a `render` method that calls the built-in default renderers, but this is completely optional and you do not need to use it. You can write your own rendering support if you are dissatisfied with the SQL produced by the default renderers. +SQL rendering is accomplished by classes that are decoupled from the SQL model classes. All the model classes have a +`render` method that calls the built-in default renderers, but this is completely optional, and you do not need to use +it. You can write your own rendering support if you are dissatisfied with the SQL produced by the default renderers. + +Writing a custom renderer is quite complex. If you want to undertake that task, we suggest that you take the time to +understand how the default renderers work first. Feel free to ask questions about this topic on the MyBatis mailing +list. + +## Writing Custom Conditions + +The library supplies a full range of conditions for all the common SQL operators (=, !=, like, between, etc.) Some +databases support extensions to the standard operators. For example, MySQL supports an extension to the "LIKE" +condition - the "ESCAPE" clause. If you need to implement a condition like that, then you will need to code a +custom condition. + +Here's an example of implementing a LIKE condition that supports ESCAPE: + +```java +@NullMarked +public class IsLikeEscape extends AbstractSingleValueCondition + implements AbstractSingleValueCondition.Filterable, AbstractSingleValueCondition.Mappable { + private static final IsLikeEscape EMPTY = new IsLikeEscape(-1, null) { + @Override + public Object value() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } + + @Override + public boolean isEmpty() { + return true; + } + }; + + public static IsLikeEscape empty() { + @SuppressWarnings("unchecked") + IsLikeEscape t = (IsLikeEscape) EMPTY; + return t; + } + + private final @Nullable Character escapeCharacter; + + protected IsLikeEscape(T value, @Nullable Character escapeCharacter) { + super(value); + this.escapeCharacter = escapeCharacter; + } + + @Override + public String operator() { + return "like"; + } + + @Override + public FragmentAndParameters renderCondition(RenderingContext renderingContext, BindableColumn leftColumn) { + var fragment = super.renderCondition(renderingContext, leftColumn); + if (escapeCharacter != null) { + fragment = fragment.mapFragment(this::addEscape); + } + + return fragment; + } + + private String addEscape(String s) { + return s + " ESCAPE '" + escapeCharacter + "'"; + } + + @Override + public IsLikeEscape filter(Predicate predicate) { + return filterSupport(predicate, IsLikeEscape::empty, this); + } + + @Override + public IsLikeEscape map(Function mapper) { + return mapSupport(mapper, v -> new IsLikeEscape<>(v, escapeCharacter), IsLikeEscape::empty); + } + + public static IsLikeEscape isLike(T value) { + return new IsLikeEscape<>(value, null); + } + + public static IsLikeEscape isLike(T value, Character escapeCharacter) { + return new IsLikeEscape<>(value, escapeCharacter); + } +} +``` + +Important notes: -Writing a custom renderer is quite complex. If you want to undertake that task, we suggest that you take the time to understand how the default renderers work first. And feel free to ask questions about this topic on the MyBatis mailing list. +1. The class extends `AbstractSingleValueCondition` - which is appropriate for like conditions +2. The class constructor accepts an escape character that will be rendered into an ESCAPE phrase +3. The class overrides `renderCondition` and changes the library generated `FragmentAndParameters` to add the ESCAPE + phrase. **This is the key to what's needed to implement a custom condition.** +4. The class implements `Filterable` and `Mappable` to provide `filter` and `map` functions as is expected for most + conditions in the library diff --git a/src/site/markdown/docs/functions.md b/src/site/markdown/docs/functions.md new file mode 100644 index 000000000..413d02840 --- /dev/null +++ b/src/site/markdown/docs/functions.md @@ -0,0 +1,26 @@ +# Database Functions + +The library supplies implementations for several common database functions. We do not try to implement a large set of functions. Rather, we supply some common functions as examples and make it relatively easy to write your own implementations of functions you might want to use that are not supplied by the library. See the page "Extending the Library" for information about how to write your own functions. + +The supplied functions are all in the `org.mybatis.dynamic.sql.select.function` package. In addition, there are static methods in the `SqlBuilder` to access all functions. + +In the following table... + +- "Function Class" is implementing class in the `org.mybatis.dynamic.sql.select.function` package +- "Example" shows the static method from the `SqlBuilder` class +- "Rendered Result" shows the rendered SQL fragment generated by the function + + +| Function Class | Example | Rendered Result | +|----------|---------|--------| +| Add | add(column1, column2, constant(55)) | column1 + column2 + 55 | +| Concatenate | concatenate(stringConstant("Name: ", column1) | 'Name: ' \|\| column1 | +| Divide | divide(column1, column2, constant(55)) | column1 / column2 / 55 | +| Lower | lower(column1) | lower(column1) | +| Multiply | multiply(column1, column2, constant(55)) | column1 * column2 * 55 | +| OperatorFunction | applyOperator("^", column1, column2) | column1 ^ column2 | +| Substring | substring(column1, 5, 7) | substring(column1, 5, 7) | +| Subtract | subtract(column1, column2, constant(55)) | column1 - column2 - 55 | +| Upper | upper(column1) | upper(column1) | + +Note especially the `OperatorFunction` - you can use this function to easily implement operators supported by your database. For example, MySQL supports a number of bitwise operators that can be easily implemented with this function. diff --git a/src/site/markdown/docs/howItWorks.md b/src/site/markdown/docs/howItWorks.md index 218475db6..51eee6960 100644 --- a/src/site/markdown/docs/howItWorks.md +++ b/src/site/markdown/docs/howItWorks.md @@ -2,7 +2,7 @@ MyBatis does four main things: -1. It executes SQL in a safe way and abstracts away all the intricacies of JDBC +1. It executes SQL safely and abstracts away all the intricacies of JDBC 2. It maps parameter objects to JDBC prepared statement parameters 3. It maps rows in JDBC result sets to objects 4. It can generate dynamic SQL with special tags in XML, or through the use of various templating engines @@ -13,7 +13,7 @@ templating engine for generating dynamic SQL. For example, MyBatis can execute an SQL string formatted like this: ```sql - select id, description from table_codes where id = #{id,jdbcType=INTEGER} + select id, description from table_codes where id = #{id,jdbcType=INTEGER} ``` This is standard SQL with a MyBatis twist - the parameter notation `#{id,jdbcType=INTEGER}` @@ -36,11 +36,11 @@ public class Parameter { public Parameter(Integer id) { this.id = id; } - + public Integer getId() { return id; } - + public String getSql() { return sql; } @@ -80,7 +80,7 @@ is designed to be the one single parameter for a MyBatis mapper method. ## What About SQL Injection? -It is true that mappers written this way are open to SQL injection. But this is also true of using any of the +It is true that mappers written this way are open to SQL injection. This is also true of using any of the various SQL provider classes in MyBatis (`@SelectProvider`, etc.) So you must be careful that these types of mappers are not exposed to any general user input. If you follow these practices, you will lower the risk of SQL injection: diff --git a/src/site/markdown/docs/insert.md b/src/site/markdown/docs/insert.md index 669954a28..6ac2e0302 100644 --- a/src/site/markdown/docs/insert.md +++ b/src/site/markdown/docs/insert.md @@ -1,25 +1,26 @@ # Insert Statements The library will generate a variety of INSERT statements: -1. An insert for a single record -1. An insert for multiple records with a single statement -1. An insert for multiple records with a JDBC batch -2. An insert with a select statement +1. An insert for a single row +1. An insert for multiple rows with a single statement +1. An insert for multiple rows with a JDBC batch +1. A general insert statement +1. An insert with a select statement -## Single Record Insert +## Single Row Insert A single record insert is a statement that inserts a single record into a table. This statement is configured differently than other statements in the library so that MyBatis' support for generated keys will work properly. To use the statement, you must first create an object that will map to the database row, then map object attributes to fields in the database. For example: ```java ... - SimpleTableRecord record = new SimpleTableRecord(); - record.setId(100); - record.setFirstName("Joe"); - record.setLastName("Jones"); - record.setBirthDate(new Date()); - record.setEmployed(true); - record.setOccupation("Developer"); - - InsertStatementProvider insertStatement = insert(record) + SimpleTableRecord row = new SimpleTableRecord(); + row.setId(100); + row.setFirstName("Joe"); + row.setLastName("Jones"); + row.setBirthDate(new Date()); + row.setEmployed(true); + row.setOccupation("Developer"); + + InsertStatementProvider insertStatement = insert(row) .into(simpleTable) .map(id).toProperty("id") .map(firstName).toProperty("firstName") @@ -41,8 +42,69 @@ Notice the `map` method. It is used to map a database column to an attribute of 3. `map(column).toStringConstant(constant_value)` will insert a constant into a column. The constant_value will be written into the generated insert statement surrounded by single quote marks (as an SQL String) 4. `map(column).toProperty(property)` will insert a value from the record into a column. The value of the property will be bound to the SQL statement as a prepared statement parameter 5. `map(column).toPropertyWhenPresent(property, Supplier valueSupplier)` will insert a value from the record into a column if the value is non-null. The value of the property will be bound to the SQL statement as a prepared statement parameter. This is used to generate a "selective" insert as defined in MyBatis Generator. +6. `map(column).toRow()` will insert the record itself into a column. This is appropriate when the "record" is a simple class like Integer or String. -### Annotated Mapper for Single Record Insert Statements +### Mapped Columns +Starting in version 2.0.0 there are two new methods: + +1. `withMappedColumn(SqlColumn)` that will map a database column to a Java property based on a property name that can + be configured in an `SQLColumn`. +2. `withMappedColumnWhenPresent(SqlColumn, Supplier)` that will map a database column to a Java property based on a + property name that can be configured in an `SQLColumn`. The insert statement will only contain the mapped column when + the Supplier returns a non-null value (this method is for single record inserts only). + +This will allow you to configure mappings in a single place (the `SqlColumn`) and reuse them in multiple insert +statements. For example: + +```java +public final class PersonDynamicSqlSupport { + public static final Person person = new Person(); + public static final SqlColumn id = person.id; + public static final SqlColumn firstName = person.firstName; + public static final SqlColumn lastName = person.lastName; + + public static final class Person extends SqlTable { + public final SqlColumn id = column("id", JDBCType.INTEGER).withJavaProperty("id"); + public final SqlColumn firstName = column("first_name", JDBCType.VARCHAR) + .withJavaProperty("firstName"); + public final SqlColumn lastName = + column("last_name", JDBCType.VARCHAR).withJavaProperty("lastName"); + + public Person() { + super("Person"); + } + } +} +``` + +In this support class, each `SqlColumn` has a configured Java property. This property can be accessed in record based +inserts in the following way: + +```java + @Test + void testRawInsert() { + try (SqlSession session = sqlSessionFactory.openSession()) { + PersonMapper mapper = session.getMapper(PersonMapper.class); + PersonRecord row = new PersonRecord(100, "Joe", "Jones"); + + InsertStatementProvider insertStatement = insert(row).into(person) + .withMappedColumn(id) + .withMappedColumn(firstName) + .withMappedColumn(lastName) + .build().render(RenderingStrategies.MYBATIS3); + + int rows = mapper.insert(insertStatement); + assertThat(rows).isEqualTo(1); + } + } +``` + +In this test, the mapping between a column and the property of a record is calculated by reading the configured Java +property for each column. + +These new methods are available for the record based insert statements (`insert`, `insertMultiple`, `insertBatch`). + +### Annotated Mapper for Single Row Insert Statements The InsertStatementProvider object can be used as a parameter to a MyBatis mapper method directly. If you are using an annotated mapper, the insert method should look like this (with @Options added for generated values if necessary): @@ -58,11 +120,11 @@ import org.mybatis.dynamic.sql.util.SqlProviderAdapter; ``` -### XML Mapper for Single Record Insert Statements +### XML Mapper for Single Row Insert Statements We do not recommend using an XML mapper for insert statements, but if you want to do so the InsertStatementProvider object can be used as a parameter to a MyBatis mapper method directly. If you are using an XML mapper, the insert method should look like this in the Java interface: - + ```java import org.mybatis.dynamic.sql.insert.render.InsertStatementProvider; @@ -81,23 +143,23 @@ The XML element should look like this (with attributes added for generated value ``` ### Generated Values -MyBatis supports returning generated values from a single record insert, or a batch insert. In either case, it is simply a matter of configuring the insert mapper method appropriately. For example, to retrieve the value of a calculated column configure your mapper method like this: +MyBatis supports returning generated values from a single row insert, or a batch insert. In either case, it is simply a matter of configuring the insert mapper method appropriately. For example, to retrieve the value of a calculated column configure your mapper method like this: ```java ... @InsertProvider(type=SqlProviderAdapter.class, method="insert") - @Options(useGeneratedKeys=true, keyProperty="record.fullName") + @Options(useGeneratedKeys=true, keyProperty="row.fullName") int insert(InsertStatementProvider insertStatement); ... ``` -The important thing is that the `keyProperty` is set correctly. It should always be in the form `record.` where `` is the attribute of the record class that should be updated with the generated value. +The important thing is that the `keyProperty` is set correctly. It should always be in the form `row.` where `` is the attribute of the record class that should be updated with the generated value. ## Multiple Row Insert Support A multiple row insert is a single insert statement that inserts multiple rows into a table. This can be a convenient way to insert a few rows into a table, but it has some limitations: -1. Since it is a single SQL statement, you could generate quite a lot of prepared statement parameters. For example, suppose you wanted to insert 1000 records into a table, and each record had 5 fields. With a multiple row insert you would generate a SQL statement with 5000 parameters. There are limits to the number of parameters allowed in a JDBC prepared statement - and this kind of insert could easily exceed those limits. If you want to insert a large number of records, you should probably use a JDBC batch insert instead (see below) -1. The performance of a giant insert statement may be less than you expect. If you have a large number of records to insert, it will almost always be more efficient to use a JDBC batch insert (see below). With a batch insert, the JDBC driver can do some optimization that is not possible with a single large statement +1. Since it is a single SQL statement, you could generate quite a lot of prepared statement parameters. For example, suppose you wanted to insert 1000 records into a table, and each record had 5 fields. With a multiple row insert you would generate a SQL statement with 5000 parameters. There are limits to the number of parameters allowed in a JDBC prepared statement - and this kind of insert could easily exceed those limits. If you want to insert many records, you should probably use a JDBC batch insert instead (see below) +1. The performance of a giant insert statement may be less than you expect. If you have many records to insert, it will almost always be more efficient to use a JDBC batch insert (see below). With a batch insert, the JDBC driver can do some optimization that is not possible with a single large statement 1. Retrieving generated values with multiple row inserts can be a challenge. MyBatis currently has some limitations related to retrieving generated keys in multiple row inserts that require special considerations (see below) Nevertheless, there are use cases for a multiple row insert - especially when you just want to insert a few records in a table and don't need to retrieve generated keys. In those situations, a multiple row insert will be an easy solution. @@ -108,7 +170,7 @@ A multiple row insert statement looks like this: try (SqlSession session = sqlSessionFactory.openSession()) { GeneratedAlwaysAnnotatedMapper mapper = session.getMapper(GeneratedAlwaysAnnotatedMapper.class); List records = getRecordsToInsert(); // not shown - + MultiRowInsertStatementProvider multiRowInsert = insertMultiple(records) .into(generatedAlways) .map(id).toProperty("id") @@ -116,7 +178,7 @@ A multiple row insert statement looks like this: .map(lastName).toProperty("lastName") .build() .render(RenderingStrategies.MYBATIS3); - + int rows = mapper.insertMultiple(multiRowInsert); } ``` @@ -141,7 +203,7 @@ import org.mybatis.dynamic.sql.util.SqlProviderAdapter; We do not recommend using an XML mapper for insert statements, but if you want to do so the MultiRowInsertStatementProvider object can be used as a parameter to a MyBatis mapper method directly. If you are using an XML mapper, the insert method should look like this in the Java interface: - + ```java import org.mybatis.dynamic.sql.insert.render.MultiInsertStatementProvider; @@ -160,17 +222,20 @@ The XML element should look like this: ``` ### Generated Values -MyBatis supports returning generated values from a multiple row insert statement with some limitations. The main limitation is that MyBatis does not support nested lists in parameter objects. Unfortunately, the `MultiRowInsertStatementProvider` relies on a nested List. It is likely this limitation in MyBatis will be removed at some point in the future, so stay tuned. +MyBatis supports returning generated values from a multiple row insert statement with some limitations. The main +limitation is that MyBatis does not support nested lists in parameter objects. Unfortunately, the +`MultiRowInsertStatementProvider` relies on a nested List. It is likely this limitation in MyBatis will be removed at +some point in the future, so stay tuned. -Nevertheless, you can configure a mapper that will work with the `MultiRowInsertStatementProvider` as created by this library. The main idea is to decompose the statement from the parameter map and send them as separate parameters to the MyBatis mapper. For example: +Nevertheless, you can configure a mapper that will work with the `MultiRowInsertStatementProvider` as created by this +library. The main idea is to decompose the statement from the parameter map and send them as separate parameters to the +MyBatis mapper. For example: ```java ... - @Insert({ - "${insertStatement}" - }) + @InsertProvider(type=SqlProviderAdapter.class, method="insertMultipleWithGeneratedKeys") @Options(useGeneratedKeys=true, keyProperty="records.fullName") - int insertMultipleWithGeneratedKeys(@Param("insertStatement") String statement, @Param("records") List records); + int insertMultipleWithGeneratedKeys(String insertStatement, @Param("records") List records); default int insertMultipleWithGeneratedKeys(MultiRowInsertStatementProvider multiInsert) { return insertMultipleWithGeneratedKeys(multiInsert.getInsertStatement(), multiInsert.getRecords()); @@ -178,7 +243,13 @@ Nevertheless, you can configure a mapper that will work with the `MultiRowInsert ... ``` -The first method above shows the actual MyBatis mapper method. Note the use of the `@Options` annotation to specify that we expect generated values. Further note that the `keyProperty` is set to `records.fullName` - in this case, `fullName` is a property of the objects in the `records` List. +The first method above shows the actual MyBatis mapper method. Note the use of the `@Options` annotation to specify +that we expect generated values. Further, note that the `keyProperty` is set to `records.fullName` - in this case, +`fullName` is a property of the objects in the `records` List. The library supplied adapter method will simply +return the `insertStatement` as supplied in the method call. The adapter method requires that there be one, and only +one, String parameter in the method call, and it assumes that this one String parameter is the SQL insert statement. +The parameter can have any name and can be specified in any position in the method's parameter list. +The `@Param` annotation is not required for the insert statement. However, it may be specified if you so desire. The second method above decomposes the `MultiRowInsertStatementProvider` and calls the first method. @@ -202,7 +273,7 @@ A batch insert is a collection of statements that can be used to execute a JDBC .build() .render(RenderingStrategies.MYBATIS3); - batchInsert.insertStatements().stream().forEach(mapper::insert); + batchInsert.insertStatements().forEach(mapper::insert); session.commit(); } @@ -211,10 +282,70 @@ A batch insert is a collection of statements that can be used to execute a JDBC It is important to open a MyBatis session by setting the executor type to BATCH. The records are inserted on the commit. You can call commit multiple times if you want to do intermediate commits. -Notice that the same mapper method that is used to insert a single record is now executed multiple times. The `map` methods are the same with the exception that the `toPropertyWhenPresent` mapping is not supported for batch inserts. +Notice that the same mapper method that is used to insert a single record is now executed multiple times. The `map` methods are the same with the exception that the `toPropertyWhenPresent` mapping is not supported for batch inserts. + +## General Insert Statement +A general insert is used to build arbitrary insert statements. The general insert does not require a separate record object to hold values for the statement - any value can be passed into the statement. This version of the insert is not convenient for retrieving generated keys with MyBatis - for that use case we recommend the "single record insert". However the general insert is perfectly acceptable for Spring JDBC template or MyBatis inserts that do not return generated keys. For example + +```java + GeneralInsertStatementProvider insertStatement = insertInto(animalData) + .set(id).toValue(101) + .set(animalName).toStringConstant("Fred") + .set(brainWeight).toConstant("2.2") + .set(bodyWeight).toValue(4.5) + .build() + .render(RenderingStrategies.MYBATIS3); +``` + +Notice the `set` method. It is used to set the value for a database column. There are several different possibilities: + +1. `set(column).toNull()` will insert a null into a column +2. `set(column).toConstant(constant_value)` will insert a constant into a column. The constant_value will be written into the generated insert statement exactly as entered +3. `set(column).toStringConstant(constant_value)` will insert a constant into a column. The constant_value will be written into the generated insert statement surrounded by single quote marks (as an SQL String) +4. `set(column).toValue(value)` will insert a value into a column. The value of the property will be bound to the SQL statement as a prepared statement parameter +5. `set(column).toValueWhenPresent(property, Supplier valueSupplier)` will insert a value into a column if the value is non-null. The value of the property will be bound to the SQL statement as a prepared statement parameter. + +### Annotated Mapper for General Insert Statements +The GeneralInsertStatementProvider object can be used as a parameter to a MyBatis mapper method directly. If you +are using an annotated mapper, the insert method should look like this: + +```java +import org.apache.ibatis.annotations.InsertProvider; +import org.mybatis.dynamic.sql.insert.render.GeneralInsertStatementProvider; +import org.mybatis.dynamic.sql.util.SqlProviderAdapter; + +... + @InsertProvider(type=SqlProviderAdapter.class, method="generalInsert") + int generalInsert(GeneralInsertStatementProvider insertStatement); +... + +``` + +### XML Mapper for General Insert Statements +We do not recommend using an XML mapper for insert statements, but if you want to do so the GeneralInsertStatementProvider object can be used as a parameter to a MyBatis mapper method directly. + +If you are using an XML mapper, the insert method should look like this in the Java interface: + +```java +import org.mybatis.dynamic.sql.insert.render.GeneralInsertStatementProvider; + +... + int generalInsert(GeneralInsertStatementProvider insertStatement); +... + +``` + +The XML element should look like this: + +```xml + + ${insertStatement} + +``` + ## Insert with Select -An insert select is an SQL insert statement the inserts the results of a select. For example: +An insert select is an SQL insert statement the inserts the results of a select statement. For example: ```java InsertSelectStatementProvider insertSelectStatement = insertInto(animalDataCopy) @@ -228,7 +359,7 @@ An insert select is an SQL insert statement the inserts the results of a select. int rows = mapper.insertSelect(insertSelectStatement); ``` -The column list is optional and can be removed if the selected columns match the layout of the table. +The column list is optional and can be removed if the selected columns match the layout of the table. ### Annotated Mapper for Insert Select Statements The InsertSelectStatementProvider object can be used as a parameter to a MyBatis mapper method directly. If you @@ -251,7 +382,7 @@ Note that MyBatis does not support overloaded mapper method names, so the name o We do not recommend using an XML mapper for insert statements, but if you want to do so the InsertSelectStatementProvider object can be used as a parameter to a MyBatis mapper method directly. If you are using an XML mapper, the insert method should look like this in the Java interface: - + ```java import org.mybatis.dynamic.sql.insert.render.InsertSelectStatementProvider; diff --git a/src/site/markdown/docs/introduction.md b/src/site/markdown/docs/introduction.md index 4b0ff10d8..32b2f9899 100644 --- a/src/site/markdown/docs/introduction.md +++ b/src/site/markdown/docs/introduction.md @@ -13,16 +13,16 @@ parameters required for that statement. The SQL statement object can be used di The library will generate these types of SQL statements: +- COUNT statements - specialized SELECT statements that return a Long value - DELETE statements with flexible WHERE clauses - INSERT statements of several types: - - A statement that inserts a single record and will insert null values into columns (a "full" insert) - - A statement that inserts a single record that will ignore null input values and their associated columns (a "selective" insert) + - A statement that inserts a single row with values supplied from a corresponding Object + - A statement that inserts a single row with values supplied directly in the statement + - A statement that inserts multiple rows using multiple VALUES clauses + - A statement that inserts multiple rows using a JDBC batch - A statement that inserts into a table using the results of a SELECT statement - - A parameter object is designed for inserting multiple objects with a JDBC batch - SELECT statements with a flexible column list, a flexible WHERE clause, and support for distinct, "group by", joins, unions, "order by", etc. -- UPDATE statements with a flexible WHERE clause. Like the INSERT statement, there are two varieties of UPDATE statements: - - A "full" update that will set null values - - A "selective" update that will ignore null input values +- UPDATE statements with a flexible WHERE clause, and flexible SET clauses The primary goals of the library are: @@ -33,8 +33,8 @@ The primary goals of the library are: 3. Flexible - where clauses can be built using any combination of and, or, and nested conditions 4. Extensible - the library will render statements for MyBatis3, Spring JDBC templates or plain JDBC. It can be extended to generate clauses for other frameworks as well. Custom where conditions can - be added easily if none of the built in conditions are sufficient for your needs. + be added easily if none of the built in conditions are sufficient for your needs. 5. Small - the library is a small dependency to add. It has no transitive dependencies. - + This library grew out of a desire to create a utility that could be used to improve the code -generated by MyBatis Generator, but the library can be used on it's own with very little setup required. +generated by MyBatis Generator, but the library can be used on its own with very little setup required. diff --git a/src/site/markdown/docs/kotlinCaseExpressions.md b/src/site/markdown/docs/kotlinCaseExpressions.md new file mode 100644 index 000000000..d0fdb4b4d --- /dev/null +++ b/src/site/markdown/docs/kotlinCaseExpressions.md @@ -0,0 +1,216 @@ +# Case Expressions in the Kotlin DSL + +Support for case expressions was added in version 1.5.1. For information about case expressions in the Java DSL, see +the [Java Case Expressions](caseExpressions.md) page. + +## Case Expressions in SQL +The library supports different types of case expressions - a "simple" case expression, and a "searched" case +expressions. Case expressions can be used in many places including select lists, order by phrases, etc. + +A simple case expression checks the values of a single column. It looks like this: + +```sql +select case id + when 1, 2, 3 then true + else false + end as small_id +from foo +``` + +Some databases also support simple comparisons on simple case expressions, which look lke this: + +```sql +select case total_length + when < 10 then 'small' + when > 20 then 'large' + else 'medium' + end as tshirt_size +from foo +``` + +A searched case expression allows arbitrary logic, and it can check the values of multiple columns. It looks like this: + +```sql +select case + when animal_name = 'Small brown bat' or animal_name = 'Large brown bat' then 'Bat' + when animal_name = 'Artic fox' or animal_name = 'Red fox' then 'Fox' + else 'Other' + end as animal_type +from foo +``` + +## Bind Variables and Casting + +The library will always render the "when" part of a case expression using bind variables. Rendering of the "then" and +"else" parts of a case expression may or may not use bind variables depending on how you write the query. In general, +the library will render "then" and "else" as constants - meaning not using bind variables. If you wish to use bind +variables for these parts of a case expressions, then you can use the `value` function to turn a constant into a +bind variable. We will show examples of the different renderings in the following sections. + +If you choose to use bind variables for all "then" and "else" values, it is highly likely that the database will +require you to specify an expected datatype by using a `cast` function. + +Even for "then" and "else" sections that are rendered with constants, you may still desire to use a `cast` in some +cases. For example, if you specify Strings for all "then" and "else" values, the database will likely return all +values as datatype CHAR with the length of the longest constant string. Typically, we would prefer the use of VARCHAR, +so we don't have to strip trailing blanks from the results. This is a good use for a `cast` with a constant. +Similarly, Kotlin float constants are often interpreted by databases as BigDecimal. You can use a `cast` to have them +returned as floats. + +Note: in the following sections we will use `?` to show a bind variable, but the actual rendered SQL will be different +because bind variables will be rendered appropriately for the execution engine you are using (either MyBatis or Spring). + +Also note: in Kotlin, `when` and `else` are reserved words - meaning we cannot use them as method names. For this +reason, the library uses `` `when` `` and `` `else` `` respectively as method names. + +Full examples for case expressions are in the test code for the library here: +https://github.com/mybatis/mybatis-dynamic-sql/blob/master/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt + +## Kotlin DSL for Simple Case Statements with Simple Values + +A simple case expression can be coded like the following in the Kotlin DSL: + +```kotlin +select(case(id) { + `when`(1, 2, 3) then true + `else`(false) + } `as` "small_id" +) { + from(foo) +} +``` + +A statement written this way will render as follows: + +```sql +select case id when ?, ?, ? then true else false end as small_id from foo +``` + +Note that the "then" and "else" parts are NOT rendered with bind variables. If you with to use bind variables, then +you can write the query as follows: + +```kotlin +select(case(id) { + `when`(1, 2, 3) then value(true) + `else`(value(false)) + } `as` "small_id" +) { + from(foo) +} +``` + +In this case, we are using the `value` function to denote a bind variable. The SQL will now be rendered as follows: + +```sql +select case id when ?, ?, ? then ? else ? end as small_id from foo +``` + +*Important*: Be aware that your database may throw an exception for SQL like this because the database cannot determine +the datatype of the resulting column. If that happens, you will need to cast one or more of the variables to the +expected data type. Here's an example of using the `cast` function: + +```kotlin +select(case(id) { + `when`(1, 2, 3) then value(true) + `else`(cast { value(false) `as` "BOOLEAN" }) + } `as` "small_id" +) { + from(foo) +} +``` + +In this case, the SQL will render as follows: + +```sql +select case id when ?, ?, ? then ? else cast(? as BOOLEAN) end as small_id from foo +``` + +In our testing, casting a single bound value is enough to inform the database of your expected datatype, but +you should perform your own testing. + +## Kotlin DSL for Simple Case Statements with Conditions + +A simple case expression can be coded like the following in the Kotlin DSL: + +```kotlin +select(case(total_length) { + `when`(isLessThan(10)) then "small" + `when`(isGreaterThan(20)) then "large" + `else`("medium") + } `as` "tshirt_size" +) { + from(foo) +} +``` + +A statement written this way will render as follows: + +```sql +select case total_length when < ? then 'small' when > ? then 'large' else 'medium' end as tshirt_size from foo +``` + +Note that the "then" and "else" parts are NOT rendered with bind variables. If you with to use bind variables, then +you can use the `value` function as shown above. + +A query like this could be a good place to use casting with constants. Most databases will return the calculated +"tshirt_size" column as CHAR(6) - so the "small" and "large" values will have a trailing blank. If you wish to use +VARCHAR, you can use the `cast` function as follows: + +```kotlin +select(case(total_length) { + `when`(isLessThan(10)) then "small" + `when`(isGreaterThan(20)) then "large" + `else`(cast { "medium" `as` "VARCHAR(6)" }) + } `as` "tshirt_size" +) { + from(foo) +} +``` + +In this case, we are using the `cast` function to specify the datatype of a constant. The SQL will now be rendered as +follows (without the line breaks): + +```sql +select case total_length + when < ? then 'small' when > ? then 'large' + else cast('medium' as VARCHAR(6)) end as tshirt_size from foo +``` + +## Kotlin DSL for Searched Case Statements + +A searched case statement is written as follows: + +```kotlin +select(case { + `when` { + animalName isEqualTo "Small brown bat" + or { animalName isEqualTo "Large brown bat"} + then("Bat") + } + `when` { + animalName isEqualTo "Artic fox" + or { animalName isEqualTo "Red fox"} + then("Fox") + } + `else`("Other") + } `as` "animal_type" +) { + from(foo) +} +``` + +The full syntax of "where" and "having" clauses is supported in the "when" clause - but that may or may not be supported +by your database. Testing is crucial. The library supports optional conditions in "when" clauses, but at least one +condition must render, else the library will throw an `InvalidSqlException`. + +The rendered SQL will be as follows (without the line breaks): +```sql +select case + when animal_name = ? or animal_name = ? then 'Bat' + when animal_name = ? or animal_name = ? then 'Fox' + else 'Other' + end as animal_type +from foo +``` + +The use of the `value` function to support bind variables, and the use of casting, is the same is shown above. diff --git a/src/site/markdown/docs/kotlinMyBatis3.md b/src/site/markdown/docs/kotlinMyBatis3.md index f546d3d23..5150f8215 100644 --- a/src/site/markdown/docs/kotlinMyBatis3.md +++ b/src/site/markdown/docs/kotlinMyBatis3.md @@ -1,38 +1,125 @@ # Kotlin Support for MyBatis3 -MyBatis Dynamic SQL includes Kotlin extension methods that enable an SQL DSL for Kotlin. This is the recommended method of using the library in Kotlin with MyBatis3. +MyBatis Dynamic SQL includes Kotlin extensions for MyBatis3 that simplify execution of statements generated by the +library. -The standard usage patterns for MyBatis Dynamic SQL and MyBatis3 in Java must be modified somewhat for Kotlin. Kotlin interfaces can contain both abstract and non-abstract methods (somewhat similar to Java's default methods in an interface). But using these methods in Kotlin based mapper interfaces will cause a failure with MyBatis because of the underlying Kotlin implementation. +The standard usage patterns for MyBatis Dynamic SQL and MyBatis3 in Java must be modified somewhat for Kotlin. +Kotlin interfaces can contain both abstract and non-abstract methods (somewhat similar to Java's default methods in +an interface). Using these methods in Kotlin based mapper interfaces will cause a failure with MyBatis because of the +underlying Kotlin implementation. -This page will show our recommended pattern for using the MyBatis Dynamic SQL with Kotlin and MyBatis3. The code shown on this page is from the `src/test/kotlin/examples/kotlin/mybatis3/canonical` directory in this repository. That directory contains a complete example of using this library with Kotlin. +This page will show our recommended pattern for using the MyBatis Dynamic SQL with Kotlin and MyBatis3. +The code shown on this page is from the `src/test/kotlin/examples/kotlin/mybatis3/canonical` directory in this +repository. That directory contains a complete example of using this library with Kotlin. -All Kotlin support is available in two packages: +All Kotlin support is available in the following packages: -* `org.mybatis.dynamic.sql.util.kotlin` - contains extension methods and utilities to enable an idiomatic Kotlin DSL for MyBatis Dynamic SQL. These objects can be used for clients using any execution target (i.e. MyBatis3 or Spring JDBC Templates) -* `org.mybatis.dynamic.sql.util.kotlin.mybatis3` - contains utlities specifically to simplify MyBatis3 based clients +* `org.mybatis.dynamic.sql.util.kotlin` - contains DSL support classes +* `org.mybatis.dynamic.sql.util.kotlin.elements` - contains the basic DSL elements common to all runtimes +* `org.mybatis.dynamic.sql.util.kotlin.mybatis3` - contains utilities specifically to simplify MyBatis3 based clients -Using the support in these packages, it is possible to create reusable Kotlin classes, interfaces, and extension methods that mimic the code created by MyBatis Generator for Java - but code that is more idiomatic for Kotlin. +Using the support in these packages, it is possible to create reusable Kotlin classes, interfaces, and extension methods +that simplify usage of the library with MyBatis3. + +The Kotlin support for MyBatis3 is implemented as utility functions that can be used with MyBatis3 mapper interfaces. +There are functions to support count, delete, insert, select, and update operations based on SQL generated by this +library. For each operation, there are two different methods of executing SQL: + +1. The first method is a two-step method. With this method you build SQL provider objects as shown on the Kotlin + overview page and then execute the generated SQL by passing the provider to a MyBatis3 mapper method +1. The second method is a one-step method that uses utility functions to combine these operations into a single step. + With this method it is common to build extension methods for MyBatis3 mappers that are specific to a table + (this is the code that MyBatis Generator will create) + +We will illustrate both approaches below. ## Kotlin Dynamic SQL Support Objects -Because Kotlin does not support static class members, we recommend a simpler pattern for creating the class containing the support objects. For example: +The pattern for the meta-model is the same as shown on the Kotlin overview page. We'll repeat it here to show some +specifics for MyBatis3. ```kotlin +import org.mybatis.dynamic.sql.AlisableSqlTable +import org.mybatis.dynamic.sql.util.kotlin.elements.column +import java.sql.JDBCType +import java.util.Date + object PersonDynamicSqlSupport { - object Person : SqlTable("Person") { - val id = column("id", JDBCType.INTEGER) - val firstName = column("first_name", JDBCType.VARCHAR) - val lastName = column("last_name", JDBCType.VARCHAR) - val birthDate = column("birth_date", JDBCType.DATE) - val employed = column("employed", JDBCType.VARCHAR, "examples.kotlin.YesNoTypeHandler") - val occupation = column("occupation", JDBCType.VARCHAR) - val addressId = column("address_id", JDBCType.INTEGER) + val person = Person() + val id = person.id + val firstName = person.firstName + val lastName = person.lastName + val birthDate = person.birthDate + val employed = person.employed + val occupation = person.occupation + val addressId = person.addressId + + class Person : AlisableSqlTable("Person", ::Person) { + val id = column(name = "id", jdbcType = JDBCType.INTEGER) + val firstName = column(name = "first_name", jdbcType = JDBCType.VARCHAR) + val lastName = column( + name = "last_name", + jdbcType = JDBCType.VARCHAR, + typeHandler = "foo.bar.LastNameTypeHandler" + ) + val birthDate = column(name = "birth_date", jdbcType = JDBCType.DATE) + val employed = column( + name = "employed", + jdbcType = JDBCType.VARCHAR, + typeHandler = "foo.bar.StringToBooleanTypeHandler" + ) + val occupation = column(name = "occupation", jdbcType = JDBCType.VARCHAR) + val addressId = column(name = "address_id", jdbcType = JDBCType.INTEGER) } } ``` +Note the use of a "type handler" on the `employed` and `lastName` columns. This allows us to use the column as a Boolean in +Kotlin, but store the values "Yes" or "No" on the database. This uses the MyBatis3 standard type handler support. + +Also note that we specify the "jdbcType" for each column. This is a best practice with MyBatis3 and will avoid errors +will nullable fields. + +## About MyBatis Mappers +Many MyBatis operations can be standardized, and you can use functionality in this library to dramatically reduce the +amount of boilerplate code you need to write. In particular, all COUNT, DELETE, and UPDATE statements can be executed +with utilities built into the library. The examples below will show how this works. + +Many INSERT statements can also be executed with built-in utilities. The only INSERT statements that require custom +coding are INSERT statements that return generated keys. For these statements, you must code a custom mapper method +with the MyBatis `@Options` annotation specifying how to retrieve generated keys. + +SELECT statements present unique challenges. One of the key functions of MyBatis is the mapping of result sets to +objects. This is a very useful capability, but it requires that the mapping between result set and object be predefined +and hard coded. Some SELECT statements can be executed without coding custom result maps. This library includes common +SELECT support with the following capabilities: + +- Execute arbitrary SELECT statements and return `List>` as the return value. This support + essentially bypasses MyBatis' result mapping capabilities and returns a low level list of results. +- Execute SELECT statements that return a single column of various types (String, Integer, BigDecimal, etc.) + +The bottom line is this - if your query returns more than one column, and you want to utilize MyBatis' result mapping +functionality, you will need to code a custom result mapping. -This object is a singleton containing the `SqlTable` and `SqlColumn` objects that map to the database table. +This library was initially conceived as a tool to improve the code created by MyBatis Generator - and the "one step" +methods shown below are based on the convention used by MyBatis generator where a set of mapper methods is created +for each table individually. If you are not using MyBatis generator, or are adding custom queries such as join +queries to an application bootstrapped with MyBatis Generator, then it is likely you will need to code custom +SELECT methods with custom result maps. -## Kotlin Mappers for MyBatis3 -If you create a Kotlin mapper interface that includes both abstract and non-abstract methods, MyBatis will be confused and throw errors. By default Kotlin does not create Java default methods in an interface. For this reason, Kotlin mapper interfaces should only contain the actual MyBatis mapper abstract interface methods. What would normally be coded as default or static methods in a mapper interface should be coded as extension methods in Kotlin. For example, a simple MyBatis mapper could be coded like this: +## Kotlin Mappers for MyBatis +The pattern we recommend involves two types of mapper methods: standard MyBatis mapper methods and extension methods. + +The standard MyBatis mapper abstract methods accept Provider objects created by the library. These methods use the +normal MyBatis annotations to specify result mappings, generated key mappings, statement types, etc. Using these +methods directly involves two steps: create the provider object, then execute the MyBatis call. + +The extension methods will reuse the abstract methods and add functionality to mappers that will build and +execute the SQL statements in a one-step process. The extension methods shown below assume that you will create +a set of CRUD methods for each table you are accessing (as is the case with code created by MyBatis Generator). + +If you create a Kotlin mapper interface that includes both abstract and non-abstract methods, MyBatis will +throw errors. By default, Kotlin does not create Java default methods in an interface. For this reason, Kotlin +mapper interfaces should only contain the actual MyBatis mapper abstract interface methods. What would normally be coded +as default or static methods in a Java mapper interface should be coded as extension methods in Kotlin. For example, +a simple MyBatis mapper could be coded like this: ```kotlin @Mapper @@ -51,36 +138,40 @@ interface PersonMapper { } ``` -And then extensions could be added to make a shortcut method as follows: +Then extensions could be added to make a shortcut method as follows: ```kotlin -private val columnList = listOf(id.`as`("A_ID"), firstName, lastName, birthDate, employed, occupation, addressId) +import org.mybatis.dynamic.sql.util.kotlin.SelectCompleter +import org.mybatis.dynamic.sql.util.kotlin.mybatis3.selectList +import org.mybatis.dynamic.sql.util.kotlin.elements.`as` + +private val columnList = listOf(id `as` "A_ID", firstName, lastName, birthDate, employed, occupation, addressId) fun PersonMapper.select(completer: SelectCompleter) = selectList(this::selectMany, columnList, Person, completer) ``` -The extension method shows the use of the `SelectCompleter` type alias. This is a DSL extension supplied with the library. We will detail its use below. For now see that the extension method can be used in client code as follows: +The extension method shows the use of the `SelectCompleter` type alias. This is a DSL extension supplied with the +library. We will detail its use below. For now see that the extension method can be used in client code to supply a +where clause and an order by clause as follows: ```kotlin val rows = mapper.select { - where(id, isLessThan(100)) - or (employed, isTrue()) { - and (occupation, isEqualTo("Developer")) + where { id isLessThan 100 } + or { + employed.isTrue() + and { occupation isEqualTo "Developer" } } orderBy(id) } ``` -This shows that the Kotlin support enables a more idiomatic Kotlin DSL. - -## Count Method Support - -A count query is a specialized select - it returns a single column - typically a long - and supports joins and a where clause. +## Count Statements -Count method support enables the creation of methods that execute a count query allowing a user to specify a where clause at runtime, but abstracting away all other details. - -To use this support, we envision creating two methods - one standard mapper method, and one extension method. The first method is the standard MyBatis Dynamic SQL method that will execute a select: +### Two-Step Method +Count statements are constructed as shown on the Kotlin overview page. These methods create a +`SelectStatementProvider` that can be executed with a MyBatis3 mapper method. MyBatis3 mappers should declare +a `count` method as follows: ```kotlin @Mapper @@ -90,24 +181,60 @@ interface PersonMapper { } ``` -This is a standard method for MyBatis Dynamic SQL that executes a query and returns a `Long`. The second method should be an extension maethod. It will reuse the abstract method and supply everything needed to build the select statement except the where clause: +This is a standard method for MyBatis Dynamic SQL that executes a query and returns a `Long`. This method can also be +implemented by using a built-in base interface as follows: ```kotlin -fun PersonMapper.count(completer: CountCompleter) = - countFrom(this::count, Person, completer) +@Mapper +interface PersonMapper : CommonCountMapper ``` -This method shows the use of `CountCompleter` which is a Kotlin typealias for a function with a receiver that will allow a user to supply a where clause. This also shows use of the Kotlin `countFrom` method which is supplied by the library. That method will build and execute the select count statement with the supplied where clause. Clients can use the method as follows: +`CommonCountMapper` can also be used on its own if you inject it into a MyBatis configuration. + +The mapper method can be used as follows: + +```kotlin +val countStatement = count() // not shown... see the overview page for examples +val mapper: PersonMapper = getMapper() // not shown +val rows: Long = mapper.count(countStatement) +``` + +### One-Step Method +You can use built-in utility functions to create mapper extension functions that simplify execution of count statements. +The extension functions will reuse the abstract method and supply everything needed to build the select statement +except the where clause: + +```kotlin +import org.mybatis.dynamic.sql.util.kotlin.CountCompleter +import org.mybatis.dynamic.sql.util.kotlin.mybatis3.count +import org.mybatis.dynamic.sql.util.kotlin.mybatis3.countDistinct +import org.mybatis.dynamic.sql.util.kotlin.mybatis3.countFrom + +fun PersonMapper.count(completer: CountCompleter) = // count(*) + countFrom(this::count, person, completer) + +fun PersonMapper.count(column: BasicColumn, completer: CountCompleter) = // count(column) + count(this::count, column, person, completer) + +fun PersonMapper.countDistinct(column: BasicColumn, completer: CountCompleter) = // count(distinct column) + countDistinct(this::count, column, person, completer) +``` + +The methods are constructed to execute count queries on one specific table - `person` in this case. + +The methods show the use of `CountCompleter` which is a Kotlin typealias for a function with a receiver that will +allow a user to supply a where clause. This also shows use of the Kotlin `countFrom`, `count`, and `countDistinct` +methods which are supplied by the library. Those methods will build and execute the select count statements with the +supplied where clause. Clients can use the methods as follows: ```kotlin val rows = mapper.count { - where(occupation, isNull()) { - and(employed, isFalse()) - } + where { occupation.isNull() } + and { employed.isFalse() } } ``` -There is also an extention method that can be used to count all rows in a table: +There is also a method that can be used to count all rows in a table: ```kotlin val rows = mapper.count { allRows() } @@ -115,30 +242,60 @@ val rows = mapper.count { allRows() } ## Delete Method Support -Delete method support enables the creation of methods that execute a delete statement allowing a user to specify a where clause at runtime, but abstracting away all other details. - -To use this support, we envision creating two methods - one standard mapper method, and one extension method. The first method is the standard MyBatis Dynamic SQL method that will execute a delete: +### Two-Step Method +Delete statements are constructed as shown on the Kotlin overview page. This method creates a +`DeleteStatementProvider` that can be executed with a MyBatis3 mapper method. MyBatis3 mappers should declare +a `delete` method as follows: ```kotlin @Mapper interface PersonMapper { - @DeleteProvider(type = SqlProviderAdapter::class, method = "delete") - fun delete(deleteStatement: DeleteStatementProvider): Int + @DeleteProvider(type = SqlProviderAdapter::class, method = "delete") + fun delete(deleteStatement: DeleteStatementProvider): Int } ``` -This is a standard method for MyBatis Dynamic SQL that executes a delete and returns an `Int` - the number of rows deleted. The second method should be an extension method. It will reuse the abstract method and supply everything needed to build the delete statement except the where clause: +This is a standard method for MyBatis Dynamic SQL that executes a query and returns an `Int` - the number of rows +deleted. This method can also be implemented by using a built-in base interface as follows: + +```kotlin +@Mapper +interface PersonMapper : CommonDeleteMapper +``` + +`CommonDeleteMapper` can also be used on its own if you inject it into a MyBatis configuration. + +The mapper method can be used as follows: + +```kotlin +val deleteStatement = deleteFrom() // not shown... see the overview page for examples +val mapper: PersonMapper = getMapper() // not shown +val rows: Int = mapper.delete(deleteStatement) +``` + +### One-Step Method +You can use built-in utility functions to create mapper extension functions that simplify execution of delete statements. +The extension functions will reuse the abstract method and supply everything needed to build the delete statement +except the where clause: ```kotlin +import org.mybatis.dynamic.sql.util.kotlin.DeleteCompleter +import org.mybatis.dynamic.sql.util.kotlin.mybatis3.deleteFrom + fun PersonMapper.delete(completer: DeleteCompleter) = - deleteFrom(this::delete, Person, completer) + deleteFrom(this::delete, person, completer) ``` -This method shows the use of `DeleteCompleter` which is a Kotlin typealias for a function with a receiver that will allow a user to supply a where clause. This also shows use of the Kotlin `deleteFrom` method which is supplied by the library. That method will build and execute the delete statement with the supplied where clause. Clients can use the method as follows: +The method is constructed to execute delete statements on one specific table - `person` in this case. + +The method shows the use of `DeleteCompleter` which is a Kotlin typealias for a function with a receiver that will +allow a user to supply a where clause. This also shows use of the Kotlin `deleteFrom` method which are supplied by the +library. Those methods will build and execute the delete statement with the supplied where clause. Clients can use the +method as follows: ```kotlin val rows = mapper.delete { - where(occupation, isNull()) + where { occupation.isNull() } } ``` @@ -148,83 +305,496 @@ There is an extension method that can be used to delete all rows in a table: val rows = mapper.delete { allRows() } ``` -## Insert Method Support +## Single Row Insert Statement -Insert method support enables the removal of some of the boilerplate code from insert methods in a mapper interfaces. - -To use this support, we envision creating several methods - two standard mapper methods, and other extension methods. The standard mapper methods are standard MyBatis Dynamic SQL methods that will execute a delete: +### Two-Step Method +Single row insert statements are constructed as shown on the Kotlin overview page. This method creates +an `InsertStatementProvider` that can be executed with a MyBatis3 mapper method. MyBatis3 mappers should declare +an `insert` method as follows: ```kotlin @Mapper interface PersonMapper { @InsertProvider(type = SqlProviderAdapter::class, method = "insert") fun insert(insertStatement: InsertStatementProvider): Int +} +``` + +This is a standard method for MyBatis Dynamic SQL that executes an insert and returns a `Int` - the number of rows +inserted. This method can also be implemented by using a built-in base interface as follows: + +```kotlin +@Mapper +interface PersonMapper : CommonInsertMapper +``` + +`CommonInsertMapper` can also be used on its own if you inject it into a MyBatis configuration. + +The mapper method can be used as follows: + +```kotlin +val insertStatement = insert() // not shown, see overview page +val mapper: PersonMapper = getMapper() // not shown +val rows: Int = mapper.insert(insertStatement) +``` + +### One-Step Method + +You can use built-in utility functions to create mapper extension functions that simplify execution of single record +insert statements. The extension functions will reuse the abstract method and supply everything needed to build the +insert statement: + +```kotlin +import org.mybatis.dynamic.sql.util.kotlin.mybatis3.insert + +fun PersonMapper.insert(row: PersonRecord) = + insert(this::insert, row, Person) { + map(id) toProperty "id" + map(firstName) toProperty "firstName" + map(lastName) toProperty "lastName" + map(birthDate) toProperty "birthDate" + map(employed) toProperty "employed" + map(occupation) toProperty "occupation" + map(addressId) toProperty "addressId" + } +``` + +This extension method reuses the mapper method and supplies all the column mappings. Clients can use the method +as follows: + +```kotlin +val record = PersonRecord(100, "Joe", LastName("Jones"), Date(), true, "Developer", 1) +val mapper: PersonMapper = getMapper() // not shown +val rows = mapper.insert(record) +``` + +### Generated Key Support + +Single record insert statements support returning a generated key using normal MyBatis generated key support. When +generated keys are expected you must code the mapper method manually and supply the `@Options` annotation that +configures generated key support. You cannot use the built-in base interface when there are generated keys. For example: + +```kotlin +interface GeneratedAlwaysMapper { + @InsertProvider(type = SqlProviderAdapter::class, method = "insert") + @Options(useGeneratedKeys = true, keyProperty = "row.id,row.fullName", keyColumn = "id,full_name") + fun insert(insertStatement: InsertStatementProvider): Int +} +``` + +This method will return two generated values in each row: `id` and `full_name`. The values will be placed into the +row properties `id` and `fullName` respectively. + +## General Insert Statement +### Two-Step Method +General insert statements are constructed as shown on the Kotlin overview page. This method creates +a `GeneralInsertStatementProvider` that can be executed with a MyBatis3 mapper method. MyBatis3 mappers should declare +a `generalInsert` method as follows: +```kotlin +@Mapper +interface PersonMapper { + @InsertProvider(type = SqlProviderAdapter::class, method = "generalInsert") + fun generalInsert(insertStatement: GeneralInsertStatementProvider): Int +} +``` + +This is a standard method for MyBatis Dynamic SQL that executes an insert and returns a `Int` - the number of rows +inserted. This method can also be implemented by using a built-in base interface as follows: + +```kotlin +@Mapper +interface PersonMapper : CommonInsertMapper +``` + +`CommonInsertMapper` can also be used on its own if you inject it into a MyBatis configuration. + +The mapper method can be used as follows: + +```kotlin +val insertStatement = insertInto() // not shown, see overview page +val mapper: PersonMapper = getMapper() // not shown +val rows: Int = mapper.generalInsert(insertStatement) +``` + +### One-Step Method + +You can use built-in utility functions to create mapper extension functions that simplify execution of general +insert statements. The extension functions will reuse the abstract method and supply everything needed to build the +insert statement except the values to insert: + +```kotlin +import org.mybatis.dynamic.sql.util.kotlin.GeneralInsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.mybatis3.insertInto + +fun PersonMapper.generalInsert(completer: GeneralInsertCompleter) = + insertInto(this::generalInsert, person, completer) +``` + +The method is constructed to execute insert statements on one specific table - `person` in this case. + +The method shows the use of `GeneralInsertCompleter` which is a Kotlin typealias for a function with a receiver that will +allow a user to supply values to insert. This also shows use of the Kotlin `insertInto` method which are supplied by the +library. Those methods will build and execute the insert statement with the supplied values. Clients can use the +method as follows: + +```kotlin +val rows = mapper.generalInsert { + set(id) toValue 100 + set(firstName) toValue "Joe" + set(lastName) toValue LastName("Jones") + set(employed) toValue true + set(occupation) toValue "Developer" + set(addressId) toValue 1 + set(birthDate) toValue Date() +} +``` + +### Generated Key Support +You cen retrieve generated keys from general insert statements if you use the two-step method. +You cannot use the built-in base interface when there are generated keys. First, code the +abstract mapper method as follows: + +```kotlin +interface GeneratedAlwaysMapper { + @InsertProvider(type = SqlProviderAdapter::class, method = "generalInsert") + @Options(useGeneratedKeys = true, keyProperty="parameters.id,parameters.fullName", keyColumn = "id,full_name") + fun generalInsert(insertStatement: GeneralInsertStatementProvider): Int +} +``` + +This method will return two generated values: `id` and `full_name`. The values will be placed into the +parameter map in the `GeneralInsertStatementProvider` with keys `id` and `fullName` respectively. + +The method can be used as follows: + +```kotlin +val mapper = getMapper() // not shown +val insertStatement = insertInto(generatedAlways) { + set(firstName).toValue("Fred") + set(lastName).toValue("Flintstone") +} + +val rows = mapper.generalInsert(insertStatement) +``` + +After the statement completes, then generated keys are available in the mapper: + +```kotlin +val id = insertStatement.parameters["id"] as Int +val fullName = insertStatement.parameters["fullName"] as String +``` + +## Multi-Row Insert Statement + +### Two-Step Method +Multi-row insert statements are constructed as shown on the Kotlin overview page. This method creates +a `MultiRowInsertStatementProvider` that can be executed with a MyBatis3 mapper method. MyBatis3 mappers should declare +an `insertMultiple` method as follows: + +```kotlin +@Mapper +interface PersonMapper { @InsertProvider(type = SqlProviderAdapter::class, method = "insertMultiple") fun insertMultiple(insertStatement: MultiRowInsertStatementProvider): Int } ``` -These methods can be used to implement simplified insert methods with Kotlin extension methods: +This is a standard method for MyBatis Dynamic SQL that executes an insert and returns a `Int` - the number of rows +inserted. This method can also be implemented by using a built-in base interface as follows: ```kotlin -fun PersonMapper.insert(record: PersonRecord) = - insert(this::insert, record, Person) { - map(id).toProperty("id") - map(firstName).toProperty("firstName") - map(lastName).toProperty("lastName") - map(birthDate).toProperty("birthDate") - map(employed).toProperty("employed") - map(occupation).toProperty("occupation") - map(addressId).toProperty("addressId") - } +@Mapper +interface PersonMapper : CommonInsertMapper +``` + +`CommonInsertMapper` can also be used on its own if you inject it into a MyBatis configuration. + +The mapper method can be used as follows: + +```kotlin +val insertStatement = insertMultiple() // not shown, see overview page +val mapper: PersonMapper = getMapper() // not shown +val rows: Int = mapper.insertMultiple(insertStatement) +``` + +### One-Step Method + +You can use built-in utility functions to create mapper extension functions that simplify execution of multi-row +insert statements. The extension functions will reuse the abstract method and supply everything needed to build the +insert statement except the values to insert: + +```kotlin +import org.mybatis.dynamic.sql.util.kotlin.mybatis3.insertMultiple fun PersonMapper.insertMultiple(vararg records: PersonRecord) = insertMultiple(records.toList()) fun PersonMapper.insertMultiple(records: Collection) = - insertMultiple(this::insertMultiple, records, Person) { - map(id).toProperty("id") + insertMultiple(this::insertMultiple, records, person) { + map(id) toProperty "id" + map(firstName) toProperty "firstName" + map(lastName) toProperty "lastName" + map(birthDate) toProperty "birthDate" + map(employed) toProperty "employed" + map(occupation) toProperty "occupation" + map(addressId) toProperty "addressId" + } +``` + +The method is constructed to execute multi-row insert statements on one specific table - `person` in this case. + +This extension method reuses the mapper method and supplies all the column mappings. Clients can use the method +as follows: + +```kotlin +val record1 = PersonRecord(100, "Joe", LastName("Jones"), Date(), true, "Developer", 1) +val record2 = PersonRecord(101, "Sarah", LastName("Smith"), Date(), true, "Architect", 2) + +val rows = mapper.insertMultiple(record1, record2) +``` + +### Generated Key Support + +Multi-row insert statements support returning a generated key using normal MyBatis generated key support. However, +generated keys require some care for multi-row insert statements. In this section we will show how to use the +library's built-in support. When generated keys are expected you must code the mapper method manually and supply the +`@Options` annotation that configures generated key support. You cannot use the built-in base interface when there are +generated keys. For example: + +```kotlin +interface GeneratedAlwaysMapper { + @InsertProvider(type = SqlProviderAdapter::class, method = "insertMultipleWithGeneratedKeys") + @Options(useGeneratedKeys = true, keyProperty="records.id,records.fullName", keyColumn = "id,full_name") + fun insertMultiple(insertStatement: String, @Param("records") records: List): Int +} +``` + +Note that this method uses a different `SQLProviderAdapter` method and also uses a decomposed version of the +provider class. This is done to code around some limitations in MyBatis3. This method will return two generated values +in each row: `id` and `full_name`. The values will be placed into the record properties `id` and `fullName` respectively. + +For the one-step method, the mapper extension method should use a different utility function as shown below: + +```kotlin +import org.mybatis.dynamic.sql.util.kotlin.mybatis3.insertMultipleWithGeneratedKeys + +fun GeneratedAlwaysMapper.insertMultiple(records: Collection): Int { + return insertMultipleWithGeneratedKeys(this::insertMultiple, records, generatedAlways) { map(firstName).toProperty("firstName") map(lastName).toProperty("lastName") - map(birthDate).toProperty("birthDate") - map(employed).toProperty("employed") - map(occupation).toProperty("occupation") - map(addressId).toProperty("addressId") } +} +``` -fun PersonMapper.insertSelective(record: PersonRecord) = - insert(this::insert, record, Person) { - map(id).toPropertyWhenPresent("id", record::id) - map(firstName).toPropertyWhenPresent("firstName", record::firstName) - map(lastName).toPropertyWhenPresent("lastName", record::lastName) - map(birthDate).toPropertyWhenPresent("birthDate", record::birthDate) - map(employed).toPropertyWhenPresent("employed", record::employed) - map(occupation).toPropertyWhenPresent("occupation", record::occupation) - map(addressId).toPropertyWhenPresent("addressId", record::addressId) - } +## Batch Insert Statement + +### Two-Step Method +Batch insert statements are constructed as shown on the Kotlin overview page. This method creates +a `BatchInsert` that can be executed with a MyBatis3 mapper method. + +Batch inserts will reuse the regular `insert` method created for single record inserts. It is also convenient to create +a method to flush the batch statements - this causes a commit and returns detailed information about the +batch such as update counts. The methods are coded as follows: + +```kotlin +import org.apache.ibatis.annotations.Flush +import org.apache.ibatis.annotations.InsertProvider +import org.apache.ibatis.executor.BatchResult + +@Mapper +interface PersonMapper { + @InsertProvider(type = SqlProviderAdapter::class, method = "insert") + fun insert(insertStatement: InsertStatementProvider): Int + + @Flush + fun flush(): List +} ``` -Note these methods use Kotlin utility methods named `insert` and `insertMultiple`. Both methods accept a function with a receiver that will allow column mappings. The methods will build and execute insert statements.= with the supplied column mappings. -Clients use these methods as follows: +These are standard methods for MyBatis. Note that the return value of the "insert" statement will NOT be the number of +rows when using batch mode operations. In batch mode the rows are not actually inserted until the statements +are flushed, or the session is committed. In batch mode, the return value is a constant with no actual meaning. +The methods can also be implemented by using a built-in base interface as follows: ```kotlin -// single insert... -val record = PersonRecord(100, "Joe", LastName("Jones"), Date(), true, "Developer", 1) -val rows = mapper.insert(record) +@Mapper +interface PersonMapper : CommonInsertMapper +``` + +`CommonInsertMapper` can also be used on its own if you inject it into a MyBatis configuration. + +MyBatis batch executions are coded as multiple invocations of a simple insert method. The difference is in the +construction of the mapper. The `SqlSession` associated with the mapper must be in "batch mode". This is accomplished +when opening the session. For example: + +```kotlin +import org.apache.ibatis.session.ExecutorType +import org.apache.ibatis.session.SqlSession +import org.apache.ibatis.session.SqlSessionFactory + +val sqlSessionFactory: SqlSessionFactory = getSessionFactory() // not shown +val sqlSession: SqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH) +val mapper: PersonMaper = sqlSession.getMapper(PersonMapper::class.java) +``` + +The mapper is now associated with a BATCH session. The mapper method can be used as follows: + +```kotlin +import org.apache.ibatis.executor.BatchResult +import org.mybatis.dynamic.sql.util.kotlin.mybatis3.insertBatch + +val sqlSessionFactory: SqlSessionFactory = getSessionFactory() // not shown +val sqlSession: SqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH) +val mapper: PersonMapper = sqlSession.getMapper(PersonMapper::class.java) + +val batchInsert = insertBatch() // not shown, see overview page +batchInsert.execute(mapper) // see note below about return value +val batchResults = mapper.flush() +``` + +Note the use of the extension function `BatchInsert.execute(mapper)`. This function simply loops over all +insert statements in the batch and executes them with the supplied mapper. Note also that +`BatchInsert.execute(mapper)` will return a `List`. However, when the mapper is in batch mode the +values in the list will not be useful. In batch mode you must execute the flush method (or `sqlSession.flushStatements()`) +to obtain update counts. The `flush` call will also commit the batch. Note that this built-in support executes all inserts +as a single transaction. If you have a large batch of records and want to process intermediate commits, you can do so +by writing code to loop through the list of insert statements obtained from `BatchInsert.insertStatements()` and execute +flush/commit as desired. + +### One-Step Method -// multiple insert... +You can use built-in utility functions to create mapper extension functions that simplify execution of batch +insert statements. The extension functions will reuse the abstract method and supply everything needed to build the +insert statement except the values to insert: + +```kotlin +import org.mybatis.dynamic.sql.util.kotlin.mybatis3.insertBatch + +fun PersonMapper.insertBatch(vararg records: PersonRecord): List = + insertBatch(records.toList()) + +fun PersonMapper.insertBatch(records: Collection): List = + insertBatch(this::insert, records, person) { + map(id) toProperty "id" + map(firstName) toProperty "firstName" + map(lastName) toProperty "lastName" + map(birthDate) toProperty "birthDate" + map(employed) toProperty "employed" + map(occupation) toProperty "occupation" + map(addressId) toProperty "addressId" + } +``` + +The method is constructed to execute batch insert statements on one specific table - `person` in this case. + +This extension method reuses the mapper method and supplies all the column mappings. Clients can use the method +as follows: + +```kotlin val record1 = PersonRecord(100, "Joe", LastName("Jones"), Date(), true, "Developer", 1) val record2 = PersonRecord(101, "Sarah", LastName("Smith"), Date(), true, "Architect", 2) -val rows = mapper.insertMultiple(record1, record2) + +val sqlSessionFactory: SqlSessionFactory = getSessionFactory() // not shown +val sqlSession: SqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH) +val mapper: PersonMapper = sqlSession.getMapper(PersonMapper::class.java) +mapper.insertBatch(record1, record2) +val batchResults = mapper.flush() ``` -## Select Method Support +### Generated Key Support + +Batch insert statements support returning a generated key using normal MyBatis generated key support. If you code +the `@Options` annotation on the `insert` statement, then the generated keys will be populated into the input records +when the transaction is committed or flushed. When generated keys are expected you must code the mapper method manually +and supply the `@Options` annotation that configures generated key support. You cannot use the built-in base interface +when there are generated keys. For example: + +```kotlin +interface GeneratedAlwaysMapper { + @InsertProvider(type = SqlProviderAdapter::class, method = "insert") + @Options(useGeneratedKeys = true, keyProperty="record.id,record.fullName", keyColumn = "id,full_name") + fun insert(insertStatement: InsertStatementProvider): Int +} +``` -Select method support enables the creation of methods that execute a query allowing a user to specify a where clause and/or an order by clause and/or pagination clauses at runtime, but abstracting away all other details. +This insert method can be used with mappers in batch mode as shown above. -To use this support, we envision creating several methods - two standard mapper methods, and other extension methods. The standard mapper methods are standard MyBatis Dynamic SQL methods that will execute a select: +## Insert Select Statement +### Two-Step Method +Insert select statements are constructed as shown on the Kotlin overview page. This method creates +an `InsertSelectStatementProvider` that can be executed with a MyBatis3 mapper method. MyBatis3 mappers should declare +a `generalInsert` method as follows: + +```kotlin +@Mapper +interface PersonMapper { + @InsertProvider(type = SqlProviderAdapter::class, method = "insertSelect") + fun insertSelect(insertSelectStatement: InsertSelectStatementProvider): Int +} +``` + +This is a standard method for MyBatis Dynamic SQL that executes an insert and returns a `Int` - the number of rows +inserted. This method can also be implemented by using a built-in base interface as follows: + +```kotlin +@Mapper +interface PersonMapper : CommonInsertMapper +``` + +`CommonInsertMapper` can also be used on its own if you inject it into a MyBatis configuration. + +The mapper method can be used as follows: + +```kotlin +val insertStatement = insertSelect() // not shown, see overview page +val mapper: PersonMapper = getMapper() // not shown +val rows: Int = mapper.insertSelect(insertStatement) +``` + +### One-Step Method +You can use built-in utility functions to create mapper extension functions that simplify execution of insert select +statements. The extension functions will reuse the abstract method and supply everything needed to build the +insert statement except the values to insert: + +```kotlin +import org.mybatis.dynamic.sql.util.kotlin.InsertSelectCompleter +import org.mybatis.dynamic.sql.util.kotlin.mybatis3.insertSelect + +fun PersonMapper.insertSelect(completer: InsertSelectCompleter) = + insertSelect(this::insertSelect, person, completer) +``` + +The method is constructed to execute insert statements on one specific table - `person` in this case. + +The method shows the use of `InsertSelectCompleter` which is a Kotlin typealias for a function with a receiver that will +allow a user to supply value column list ans select statement. This also shows use of the Kotlin `insertSelect` method +which is supplied by the library. This method will build and execute the insert statement with the supplied column +list and select statement. Clients can use the method as follows: + +```kotlin +val mapper = getMapper() // not shown +val rows = mapper.insertSelect { + columns(id, firstName, lastName, employed, occupation, addressId, birthDate) + select(add(id, constant("100")), firstName, lastName, employed, occupation, addressId, birthDate) { + from(person) + orderBy(id) + } +} +``` + +### Generated Key Support + +Generated keys with insert select are not directly supported by library utilities and can be quite challenging. +There are examples in the source repository if you have a need to do this. + +## Select Method Support + +### Two-Step Method +Select statements are constructed as shown on the Kotlin overview page. Those methods create a +`SelectStatementProvider` that can be executed with MyBatis3 mapper methods. We recommend creating two mapper methods: +one that returns a list of records, and another that returns a single record or null: ```kotlin @Mapper @@ -248,10 +818,37 @@ interface PersonMapper { } ``` -These methods can be used to create simplified select methods with Kotlin extension methods: +Note that the result map is shared between the two methods. + +The methods can be used as follows: + +```kotlin +val mapper: PersonMapper = getMapper() // not shown + +val selectStatement = select() // not shown... see the overview page for examples +val rows: List = mapper.selectMany(selectStatement) + +val selectOneStatement = select() // not shown... see the overview page for examples +val row: PersonRecord? = mapper.selectOne(selectStatement) +``` + +Note that the select statement is the same whether multiple or single rows are expected. Also note that a select +distinct can be executed with the `selectMany` method. + +### One-Step Method +You can use built-in utility functions to create mapper extension functions that simplify execution of select statements. +The extension functions will reuse the abstract methods and supply the table and column list for the statement. +We recommend three extension methods for select multiple records, select multiple records with the distinct keyword, +and selecting a single record: ```kotlin -private val columnList = listOf(id.`as`("A_ID"), firstName, lastName, birthDate, employed, occupation, addressId) +import org.mybatis.dynamic.sql.util.kotlin.SelectCompleter +import org.mybatis.dynamic.sql.util.kotlin.elements.`as` +import org.mybatis.dynamic.sql.util.kotlin.mybatis3.selectDistinct +import org.mybatis.dynamic.sql.util.kotlin.mybatis3.selectList +import org.mybatis.dynamic.sql.util.kotlin.mybatis3.selectOne + +private val columnList = listOf(id `as` "A_ID", firstName, lastName, birthDate, employed, occupation, addressId) fun PersonMapper.selectOne(completer: SelectCompleter) = selectOne(this::selectOne, columnList, Person, completer) @@ -263,28 +860,42 @@ fun PersonMapper.selectDistinct(completer: SelectCompleter) = selectDistinct(this::selectMany, columnList, Person, completer) ``` -These methods show the use of `SelectCompleter` which is a which is a Kotlin typealias for a function with a receiver that will allow a user to supply a where clause. The `selectMany` method can be used to implement generalized select methods where a user can specify a where clause and/or an order by clause. Typically we recommend two of these methods - for select, and select distinct. The `selectOne` method is used to create a generalized select method where a user can specify a where clause. These methods also show the use of the built in Kotlin functions `selectDistinct`, `selectList`, and `selectOne`. These functions build and execute select statements, and help to avoid platform type issues in Kotlin. They enable the Kotlin compiler to correctly infer the result type (either `PersonRecord?` or `List` in this case). +The methods are constructed to execute select statements on one specific table - `person` in this case - and with a fixed +column list that matches the MyBatis result mapping. -The general `selectOne` method can also be used to implement a `selectByPrimaryKey` method: +The methods show the use of `SelectCompleter` which is a Kotlin typealias for a function with a receiver that will +allow a user to supply a where clause. This also shows use of the Kotlin `selectDistinct`, `selectList`, and `selectOne` +methods which are supplied by the library. Those methods will build and execute the select statement. Clients can use +the methods as follows: ```kotlin -fun PersonMapper.selectByPrimaryKey(id_: Int) = - selectOne { - where(id, isEqualTo(id_)) - } -``` - -Clients can use the methods as follows: +val mapper = getMapper() // not shown +val distinctRecords = mapper.selectDistinct { + where { id isGreaterThan 5 } +} -```kotlin val rows = mapper.select { - where(firstName, isIn("Fred", "Barney")) + where { firstName.isIn("Fred", "Barney") } orderBy(id) limit(3) } + +val record = mapper.selectOne { + where { id isEqualTo 1 } +} + +``` + +The general `selectOne` method can also be used to implement a `selectByPrimaryKey` method: + +```kotlin +fun PersonMapper.selectByPrimaryKey(id_: Int) = + selectOne { + where { id isEqualTo id_ } + } ``` -There is a utility methods that will select all rows in a table: +There is a utility method that will select all rows in a table: ```kotlin val rows = mapper.select { allRows() } @@ -298,12 +909,51 @@ val rows = mapper.select { orderBy(lastName, firstName) } ``` +### Join Support -## Update Method Support +You can implement functions that support a reusable select method based on a join. In this way, you can create +the start of the select statement (the column list and join specifications) and allow the user to supply where clauses +and other parts of a select statement. For example, you could code a mapper extension method like this: -Update method support enables the creation of methods that execute an update allowing a user to specify SET clauses and/or a WHERE clause, but abstracting away all other details. +```kotlin +fun PersonWithAddressMapper.select(completer: SelectCompleter): List = + select( + id `as` "A_ID", firstName, lastName, birthDate, + employed, occupation, address.id, address.streetAddress, address.city, address.state + ) { + from(person, "p") + fullJoin(address) { + on(person.addressId) equalTo address.id + } + completer() + }.run(this::selectMany) +``` -To use this support, we envision creating several methods - one standard mapper method, and other extension methods. The standard mapper method is a standard MyBatis Dynamic SQL methods that will execute an update: +This method creates the start of a select statement with a join, and accepts user input to complete the statement. +This shows reuse of a regular MyBatis mapper method - `selectMany` as shown above - with a result map that matches the +select list. Like other select methods, this method can be used as follows: + +```kotlin +val records = mapper.select { + where { id isLessThan 100 } + limit(5) +} +``` + +## Multi-Select Statement Support + +Multi-select statements are a special case of select statement. All the above information about MyBatis mappers applies +equally to multi-select statements. + +The library does not provide a "one-step" shortcut for multi-select queries. You can execute a multi-select query +with the two-step method using either a "selectMany" or "selectOne" mapper method as shown above. + +## Update Method Support + +### Two-Step Method +Update statements are constructed as shown on the Kotlin overview page. This method creates an +`UpdateStatementProvider` that can be executed with a MyBatis3 mapper method. MyBatis3 mappers should declare +an `update` method as follows: ```kotlin @Mapper @@ -313,73 +963,114 @@ interface PersonMapper { } ``` -This is a standard method for MyBatis Dynamic SQL that executes an update and returns an `int` - the number of rows updated. The extension methods will reuse this method and supply everything needed to build the update statement except the values and the where clause: +This is a standard method for MyBatis Dynamic SQL that executes an update and returns an `Int` - the number of rows +updated. This method can also be implemented by using a built-in base interface as follows: + +```kotlin +@Mapper +interface PersonMapper : CommonUpdateMapper +``` + +`CommonUpdateMapper` can also be used on its own if you inject it into a MyBatis configuration. + +The mapper method can be used as follows: + +```kotlin +val updateStatement = update() // not shown... see the overview page for examples +val mapper: PersonMapper = getMapper() // not shown +val rows: Int = mapper.update(updateStatement) +``` + +### One-Step Method +You can use built-in utility functions to create mapper extension functions that simplify execution of update statements. +The extension functions will reuse the abstract method and supply everything needed to build the update statement +except the set and where clauses: ```kotlin +import org.mybatis.dynamic.sql.util.kotlin.UpdateCompleter +import org.mybatis.dynamic.sql.util.kotlin.mybatis3.update + fun PersonMapper.update(completer: UpdateCompleter) = - update(this::update, Person, completer) + update(this::update, person, completer) ``` -This extension method shows the use of `UpdateCompleter` which is a Kotlin typealias for a function with a receiver that will allow a user to supply values and a where clause. This also shows use of the Kotlin `update` method which is supplied by the library. That method will build and execute the update statement with the supplied values and where clause. Clients can use the method as follows: +The method is constructed to execute update statements on one specific table - `person` in this case. + +The method shows the use of `UpdateCompleter` which is a Kotlin typealias for a function with a receiver that will +allow a user to supply set phrases and a where clause. This also shows use of the Kotlin `update` method which is +supplied by the library. Those methods will build and execute the update statement with the supplied set phrases and +where clause. Clients can use the method as follows: ```kotlin val rows = mapper.update { set(occupation).equalTo("Programmer") - where(id, isEqualTo(100)) + where { id isEqualTo 100 } + and(firstName, isEqualTo("Joe")) } ``` -All rows in a table can be updated by simply omitting the where clause: +If you wish to update all rows in a table, you can simply omit the where clause as follows: ```kotlin +// update all rows... val rows = mapper.update { - set(occupation).equalTo("Programmer") + set(occupation) equalTo "Programmer" } ``` -It is also possible to write a utility method that will set values. For example: +It is also possible to write utility methods that will set values. For example: ```kotlin fun KotlinUpdateBuilder.updateSelectiveColumns(record: PersonRecord) = apply { - set(id).equalToWhenPresent(record::id) - set(firstName).equalToWhenPresent(record::firstName) - set(lastName).equalToWhenPresent(record::lastName) - set(birthDate).equalToWhenPresent(record::birthDate) - set(employed).equalToWhenPresent(record::employed) - set(occupation).equalToWhenPresent(record::occupation) - set(addressId).equalToWhenPresent(record::addressId) + set(id) equalToWhenPresent record::id + set(firstName) equalToWhenPresent record::firstName + set(lastName) equalToWhenPresent record::lastName + set(birthDate) equalToWhenPresent record::birthDate + set(employed) equalToWhenPresent record::employed + set(occupation) equalToWhenPresent record::occupation + set(addressId) equalToWhenPresent record::addressId } ``` -This method will selectively set values if corresponding fields in a record are non null. This method can be used as follows: +This method will selectively set values if corresponding fields in a record are non-null. This method can be used as +follows: ```kotlin val rows = mapper.update { updateSelectiveColumns(updateRecord) - where(id, isEqualTo(100)) + where { id isEqualTo 100 } } ``` -## Join Support -There are extension functions that support building a reusable select method based on a join. In this way, you can create the start of the select statement (the column list and join specifications) and allow the user to supply where clauses and other parts of a select statement. For example, you could code a mapper extension method like this: +If you wish to implement an "update by primary key" method, you can reuse the extension method as follows: ```kotlin -fun PersonWithAddressMapper.select(completer: SelectCompleter): List { - val start = select(columnList).from(Person, "p") { - join(Address, "a") { - on(Person.addressId, equalTo(Address.id)) - } +fun PersonMapper.updateByPrimaryKey(record: PersonRecord) = + update { + set(firstName) equalToOrNull record::firstName + set(lastName) equalToOrNull record::lastName + set(birthDate) equalToOrNull record::birthDate + set(employed) equalToOrNull record::employed + set(occupation) equalToOrNull record::occupation + set(addressId) equalToOrNull record::addressId + where { id isEqualTo record.id!! } + } + +fun PersonMapper.updateByPrimaryKeySelective(record: PersonRecord) = + update { + set(firstName) equalToWhenPresent record::firstName + set(lastName) equalToWhenPresent record::lastName + set(birthDate) equalToWhenPresent record::birthDate + set(employed) equalToWhenPresent record::employed + set(occupation) equalToWhenPresent record::occupation + set(addressId) equalToWhenPresent record::addressId + where { id isEqualTo record.id!! } } - return selectList(this::selectMany, start, completer) -} ``` -This method creates the start of a select statement with a join, and accepts user input to complete the statement. This shows use of and overloaded `selectList` method that accepts the start of a select statement and a completer. Like other select methods, this method can be used as follows: +The method `updateByPrimaryKey` will update every column - if a property in the record is null, the column will be set +to null. -```kotlin -val records = mapper.select { - where(id, isLessThan(100)) - limit(5) -} -``` +The method `updateByPrimaryKeySelective` will update every column that has a non-null corresponding property +in the record. If a property in the record is null, the column will not be updated. diff --git a/src/site/markdown/docs/kotlinOverview.md b/src/site/markdown/docs/kotlinOverview.md new file mode 100644 index 000000000..78dba6a78 --- /dev/null +++ b/src/site/markdown/docs/kotlinOverview.md @@ -0,0 +1,531 @@ +# Kotlin Support + +MyBatis Dynamic SQL includes Kotlin extensions that provide an SQL DSL for Kotlin. This is the recommended method +of using the library with Kotlin. For the most part, the Kotlin DSL provides a thin wrapper over the underlying Java +DSL. You certainly can use the Java DSL with Kotlin. However, using the more specific Kotlin DSL will provide some +benefits: + +1. The Kotlin DSL generally masks the platform types that are inferred with the underlying Java DSL +2. The Kotlin DSL accurately expresses the nullability expectations of the underlying Java DSL +3. Using the Kotlin DSL will avoid some confusion with overloaded function names that are present in the Java DSL +4. The Kotlin DSL makes extensive use of Kotlin DSL construction features. It more closely mimics actual SQL than the + Java DSL and will likely feel more natural to Kotlin developers + +We take the customary approach to DSL building in Kotlin in that we attempt to create a somewhat natural feel for SQL, +but not an exact replacement of SQL. The Kotlin DSL relies on the capabilities of the underlying Java DSL. This means +that the Kotlin DSL does not add any capabilities that are not already present in the Java DSL. +You can continue to use the underlying Java DSL at any time - it functions properly in Kotlin. One of the main features +of the Kotlin DSL is that we move away from the method chaining paradigm of the Java DSL and move towards a more +idiomatic Kotlin DSL based on lambdas and receiver objects. We think the Kotlin DSL feels more natural - certainly it +is a more natural experience for Kotlin. + +One consequence of the more natural feel of the Kotlin DSL is that you are free to write unusual looking SQL. For +example, you could write a SELECT statement with a WHERE clause after a UNION. Most of the time these unusual usages +of the DSL will yield correct results. However, it would be best to use the DSL as shown below to avoid hard to +diagnose problems. + +If you plan to use the Kotlin DSL, we recommend that you do not use any function from +`org.mybatis.dynamic.sql.SqlBuilder` (the Java DSL entry points). Many functions from that class have been duplicated +for the Kotlin DSL, but in a more Kotlin native manner. + +## Package Structure + +We have implemented all Kotlin DSL functions as "top level" functions in their respective packages, so they can be used +with a wildcard import statement. Until you become more familiar with the package structure, it is easiest to +simply import the packages based on the type of object you wish to create. + +To fully understand the package structure, it is important to understand the different types of objects that can be +generated by the DSL. In general, the DSL can be used to generate the following types of objects: + +1. "Model" objects are generated by the DSL, but are not rendered into a "provider". For most + users these objects can be considered intermediate objects and will not need to be accessed directly. However, if + you want to implement a custom rendering strategy then you might need to work with "model" objects (this is an + unusual use case) +2. "Provider" objects have been rendered into a form that can be used with SQL execution engines + directly. Currently, the library supports rendering for MyBatis3 and Spring JDBC Template. Most users will interact + with "provider" objects in some form or another + +When creating model objects, import the following packages: + +```kotlin +import org.mybatis.dynamic.sql.util.kotlin.elements.* +import org.mybatis.dynamic.sql.util.kotlin.model.* +``` + +When creating provider objects rendered for MyBatis3 (and using other MyBatis3 specific functions), import the following +packages: + +```kotlin +import org.mybatis.dynamic.sql.util.kotlin.elements.* +import org.mybatis.dynamic.sql.util.kotlin.mybatis3.* +``` + +When creating provider objects rendered for Spring JDBC Template (and using other Spring specific functions), import +the following packages: + +```kotlin +import org.mybatis.dynamic.sql.util.kotlin.elements.* +import org.mybatis.dynamic.sql.util.kotlin.spring.* +``` + +Every example shown on this page will compile and run accurately with either set of import statements. The only +difference is the final object type produced by the library. + +## Kotlin Dynamic SQL Support Objects + +MyBatis Dynamic SQL relies on a database "meta model" - objects that describe database tables and columns. + +The pattern shown below is similar to the pattern recommended with Java. +Kotlin does not support static class members, so the pattern for Kotlin varies a bit from Java by using a combination +of Kotlin `object` and `class`. Like the Java pattern, this pattern will allow you to use table and column names +in a "qualified" or "un-qualified" manner that looks like natural SQL. For example, in the +following a column could be referred to as `firstName` or `person.firstName`. + +```kotlin +import org.mybatis.dynamic.sql.AlisableSqlTable +import org.mybatis.dynamic.sql.util.kotlin.elements.column +import java.util.Date + +object PersonDynamicSqlSupport { + val person = Person() + val id = person.id + val firstName = person.firstName + val lastName = person.lastName + val birthDate = person.birthDate + val employed = person.employed + val occupation = person.occupation + + class Person : AlisableSqlTable("Person", ::Person) { + val id = column(name = "id", jdbcType = JDBCType.INTEGER) + val firstName = column(name = "first_name", jdbcType = JDBCType.VARCHAR) + val lastName = column(name = "last_name", jdbcType = JDBCType.VARCHAR) + val birthDate = column(name = "birth_date", jdbcType = JDBCType.DATE) + val employed = column( + name = "employed", + jdbcType = JDBCType.VARCHAR, + typeHandler = "examples.kotlin.mybatis3.canonical.YesNoTypeHandler" + ) + val occupation = column(name = "occupation", jdbcType = JDBCType.VARCHAR) + val addressId = column(name = "address_id", jdbcType = JDBCType.INTEGER) + } +} +``` + +Notes: + +1. The outer object is a singleton containing the `AlisableSqlTable` and `SqlColumn` objects that map to the database table. +2. The inner `AlisableSqlTable` is declared as a `class` rather than an `object` - this allows you to create additional + instances for use in self-joins. +3. Note the use of the `column` extension function. This function accepts different + parameters for the different attributes that can be assigned to a column (such as a MyBatis3 type handler, or a + custom rendering strategy). We recommend using this extension function rather than the corresponding `column` and + `withXXX` methods in the Java native DSL because the extension method will retain the non-nullable type information + associated with the column. + +## Statements + +The DSL will generate a wide variety of SQL statements. We'll cover the details below with examples for each +statement type. Included with both the MyBatis3 and Spring support are additional features specific to those platforms, +we will cover those additions on separate pages. On this page, we'll cover what is common for all platforms. + +The library supports the following types of statements: + +1. Count statements of various types - these are specialized select statements that return a single Long column, Count + statements support where clauses, joins, and subqueries. +2. Delete statement with or without a where clause. +3. Insert statements of various types: + 1. Single row insert - a statement where the insert values are obtained from a record class + 2. General insert - a statement where the insert values are set directly in the statement + 3. Multi-row Insert - a statement where the insert values are derived from a collection of records + 4. Batch insert - a set of insert statements appropriate for use as a JDBC batch + 5. Insert select - a statement where the insert values are obtained from a select statement +4. Select statement that supports joins, subqueries, where clauses, order by clauses, group by clauses, etc. +5. Multi-Select statements - multiple full select statements (including order by and paging clauses) merged together + with "union" or "union all" operators +6. Update Statement with or without a where clause + +## Count Statements + +A count statement is a specialized select - it returns a single column - typically a long - and supports joins and a +where clause. + +The library supports three types of count statements: + +1. `count(*)` - counts the number of rows that match a where clause +2. `count(column)` - counts the number of non-null column values that match a where clause +3. `count(distinct column)` - counts the number of unique column values that match a where clause + +The DSL for count statements looks like this: + +```kotlin +// count(*) +val countRowsStatement = countFrom(person) { + where { id isLessThan 4 } +} + +// count(column) +val countColumnStatement = count(lastName) { + from(person) +} + +// count(distinct column) +val countDistinctColumnStatement = countDistinct(lastName) { + from(person) +} +``` + +These methods create models or providers depending on which package is used: + +| Package | Resulting Object | +|----------------------------------------------|---------------------------------------------------------------------------------------| +| org.mybatis.dynamic.sql.util.kotlin.model | org.mybatis.dynamic.sql.select.SelectModel | +| org.mybatis.dynamic.sql.util.kotlin.mybatis3 | org.mybatis.dynamic.sql.select.render.SelectStatementProvider (rendered for MyBatis3) | +| org.mybatis.dynamic.sql.util.kotlin.spring | org.mybatis.dynamic.sql.select.render.SelectStatementProvider (rendered for Spring) | + +## Delete Statement + +Delete statement support enables the creation of arbitrary delete statements including where clauses. + +The DSL for delete statements looks like this: + +```kotlin +val deleteStatement = deleteFrom(person) { + where { id isLessThan 4 } +} +``` + +There is also a method that can be used to delete all rows in a table: + +```kotlin +val rows = template.deleteFrom(person) { + allRows() +} +``` + +This method creates models or providers depending on which package is used: + +| Package | Resulting Object | +|----------------------------------------------|---------------------------------------------------------------------------------------| +| org.mybatis.dynamic.sql.util.kotlin.model | org.mybatis.dynamic.sql.delete.DeleteModel | +| org.mybatis.dynamic.sql.util.kotlin.mybatis3 | org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider (rendered for MyBatis3) | +| org.mybatis.dynamic.sql.util.kotlin.spring | org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider (rendered for Spring) | + +## Single Row Insert Statement + +This method support enables the creation of arbitrary insert statements given a class that matches a database row. +If you do not with to create such a class, then see the general insert support following this section. + +The DSL for insert statements looks like this: + +```kotlin +data class PersonRecord( + val id: Int, + val firstName: String, + val lastName: String, + val birthDate: Date, + val employed: Boolean, + val occupation: String?, + val addressId: Int +) + +val row = PersonRecord(100, "Joe", "Jones", Date(), true, "Developer", 1) + +val insertRecordStatement = insert(row) { + into(person) + map(id) toProperty "id" + map(firstName) toProperty "firstName" + map(lastName) toProperty "lastName" + map(birthDate) toProperty "birthDate" + map(employed) toProperty "employedAsString" + map(occupation).toPropertyWhenPresent("occupation", row::occupation) + map(addressId) toProperty "addressId" +} +``` + +This statement maps insert columns to properties in a class. Note the use of the `toPropertyWhenPresent` mapping - this +will only set the insert value if the value of the property is non-null. Also note that you can use other mapping +methods to map insert fields to nulls and constants if desired. Many of the mappings can be called via infix +as shown above. + +This method creates models or providers depending on which package is used: + +| Package | Resulting Object | +|----------------------------------------------|---------------------------------------------------------------------------------------| +| org.mybatis.dynamic.sql.util.kotlin.model | org.mybatis.dynamic.sql.insert.InsertModel | +| org.mybatis.dynamic.sql.util.kotlin.mybatis3 | org.mybatis.dynamic.sql.insert.render.InsertStatementProvider (rendered for MyBatis3) | +| org.mybatis.dynamic.sql.util.kotlin.spring | org.mybatis.dynamic.sql.insert.render.InsertStatementProvider (rendered for Spring) | + +## General Insert Statement + +General insert method support enables the creation of arbitrary insert statements and does not require the creation of +a class matching the database row. + +The DSL for general insert statements looks like this: + +```kotlin +val generalInsertStatement = insertInto(person) { + set(id) toValue 100 + set(firstName) toValue "Joe" + set(lastName) toValue "Jones" + set(birthDate) toValue Date() + set(employed) toValue true + set(occupation) toValue "Developer" + set(addressId) toValue 1 +} +``` + +This method creates models or providers depending on which package is used: + +| Package | Resulting Object | +|----------------------------------------------|----------------------------------------------------------------------------------------------| +| org.mybatis.dynamic.sql.util.kotlin.model | org.mybatis.dynamic.sql.insert.GeneralInsertModel | +| org.mybatis.dynamic.sql.util.kotlin.mybatis3 | org.mybatis.dynamic.sql.insert.render.GeneralInsertStatementProvider (rendered for MyBatis3) | +| org.mybatis.dynamic.sql.util.kotlin.spring | org.mybatis.dynamic.sql.insert.render.GeneralInsertStatementProvider (rendered for Spring) | + +## Multi-Row Insert Statement + +Multi-row inserts allow you to insert multiple rows into a table with a single insert statement. This is a convenient +way to insert multiple rows, but there are some limitations. Multi-row inserts are not intended for large bulk inserts +because it is possible to create insert statements that exceed the number of prepared statement parameters allowed in +JDBC. For bulk inserts, please consider using a JDBC batch (see below). + +Note the distinction between multi-row inserts and batch inserts. A multi-row insert is a single insert statement that +inserts multiple rows into the database. It is formatted as follows: + +```sql +insert into baz (foo, bar) values (1, 2), (3, 4), (5, 6) +``` + +A multi-row insert is a single insert statement with many (perhaps very many) parameters. Multi-row inserts are viewed +as a single transaction by the database. In addition, most JDBC drivers place some limit on the number of prepared +statement parameters. So it is best to use a multi-row insert with a small number of rows. + +The DSL for multi-row insert statements looks like this: + +```kotlin +val record1 = PersonRecord(100, "Joe", "Jones", Date(), true, "Developer", 1) +val record2 = PersonRecord(101, "Sarah", "Smith", Date(), true, "Architect", 2) + +val multiRowInsertStatement = insertMultiple(listOf(record1, record2)) { + into(person) + map(id) toProperty "id" + map(firstName) toProperty "firstName" + map(lastName) toProperty "lastNameAsString" + map(birthDate) toProperty "birthDate" + map(employed) toProperty "employedAsString" + map(occupation) toProperty "occupation" + map(addressId) toProperty "addressId" +} +``` + +Note there is no `toPropertyWhenPresent` mapping available on a multi-row insert. + +Also note that there is no overload of this function that accepts a vararg of rows because it would cause an overload +resolution ambiguity error. This limitation is overcome in the utility functions for MyBatis and Spring as shown on +the documentation pages for those utilities. + +This method creates models or providers depending on which package is used: + +| Package | Resulting Object | +|----------------------------------------------|-----------------------------------------------------------------------------------------------| +| org.mybatis.dynamic.sql.util.kotlin.model | org.mybatis.dynamic.sql.insert.MultiRowInsertModel | +| org.mybatis.dynamic.sql.util.kotlin.mybatis3 | org.mybatis.dynamic.sql.insert.render.MultiRowInsertStatementProvider (rendered for MyBatis3) | +| org.mybatis.dynamic.sql.util.kotlin.spring | org.mybatis.dynamic.sql.insert.render.MultiRowInsertStatementProvider (rendered for Spring) | + +## Batch Insert Statement + +A batch insert is a sequence of insert statements that can be handled as a batch by the JDBC driver. Batches +have virtually no limit on the number of statements that can be executed. Batches also support intermediate commits. +Some care must be taken with the underlying database engine to ensure that batch statements are executed as a +batch and not just a collection of individual inserts. This is especially true with MyBatis. Spring has support +for executing a batch with a single commit. Intermediate commits must be handled manually. + +MyBatis and Spring have different ways of executing batch inserts - you can see details on those specific pages. The +library generates objects that can be used by either MyBatis or Spring. + +The DSL for batch insert statements looks like this: + +```kotlin +val record1 = PersonRecord(100, "Joe", "Jones", Date(), true, "Developer", 1) +val record2 = PersonRecord(101, "Sarah", "Smith", Date(), true, "Architect", 2) + +val batchInsertStatement = insertBatch(listOf(record1, record2)) { + into(person) + map(id) toProperty "id" + map(firstName) toProperty "firstName" + map(lastName) toProperty "lastNameAsString" + map(birthDate) toProperty "birthDate" + map(employed) toProperty "employedAsString" + map(occupation) toProperty "occupation" + map(addressId) toProperty "addressId" +} +``` + +Note there is no `toPropertyWhenPresent` mapping available on a batch insert. + +Also note that there is no overload of this function that accepts a vararg of rows because it would cause an overload +resolution ambiguity error. This limitation is overcome in the utility functions for MyBatis and Spring as shown on +the documentation pages for those utilities. + +This method creates models or providers depending on which package is used: + +| Package | Resulting Object | +|----------------------------------------------|---------------------------------------------------------------------------| +| org.mybatis.dynamic.sql.util.kotlin.model | org.mybatis.dynamic.sql.insert.BatchInsertModel | +| org.mybatis.dynamic.sql.util.kotlin.mybatis3 | org.mybatis.dynamic.sql.insert.render.BatchInsert (rendered for MyBatis3) | +| org.mybatis.dynamic.sql.util.kotlin.spring | org.mybatis.dynamic.sql.insert.render.BatchInsert (rendered for Spring) | + +## Insert Select Statement + +An insert select statement obtains insert values from a nested select statement. + +The DSL for an insert select statement looks like this: + +```kotlin +val insertSelectStatement = insertSelect(person) { + columns(id, firstName, lastName, birthDate, employed, occupation, addressId) + select( + add(id, constant("100")), firstName, lastName, birthDate, employed, occupation, addressId + ) { + from(person) + where { employed.isTrue() } + } +} +``` + +The `columns` method accepts a list of `SqlColumn` objects that are rendered as the columns to insert. The `select` +method is a query whose value list should match the `columns`. The number of rows inserted will generally match the +number of rows returned from the query. + +This method creates models or providers depending on which package is used: + +| Package | Resulting Object | +|----------------------------------------------|---------------------------------------------------------------------------------------------| +| org.mybatis.dynamic.sql.util.kotlin.model | org.mybatis.dynamic.sql.insert.InsertSelectModel | +| org.mybatis.dynamic.sql.util.kotlin.mybatis3 | org.mybatis.dynamic.sql.insert.render.InsertSelectStatementProvider (rendered for MyBatis3) | +| org.mybatis.dynamic.sql.util.kotlin.spring | org.mybatis.dynamic.sql.insert.render.InsertSelectStatementProvider (rendered for Spring) | + +## Select Statement + +Select statement support enables the creation of methods that execute a query allowing a user to specify a where clause, +join specifications, order by clauses, group by clauses, pagination clauses, etc. + +The full DSL for select statements looks like this: + +```kotlin +val selectStatement = select(orderMaster.orderId, orderMaster.orderDate, orderDetail.lineNumber, + orderDetail.description, orderDetail.quantity +) { + from(orderMaster, "om") + join(orderDetail, "od") on { + orderMaster.orderId isEqualTo orderDetail.orderId + and { orderMaster.orderId isEqualTo orderDetail.orderId } + } + where { orderMaster.orderId isEqualTo 1 } + or { + orderMaster.orderId isEqualTo 2 + and { orderDetail.quantity isLessThan 6 } + } + orderBy(orderMaster.orderId) + limit(3) +} +``` + +In a select statement you must specify a table in a `from` clause. Everything else is optional. + +Multiple join clauses can be specified if you need to join additional tables. Full, left, right, inner, +and outer joins are supported. + +Where clauses can be of arbitrary complexity and support all SQL operators including exists operators, subqueries, etc. +You can nest `and`, `or`, and `not` clauses as necessary in where clauses. + +There is also a method that will create a "distinct" query (`select distinct ...`) as follows: + +```kotlin +val selectStatement = selectDistinct(id, firstName, lastName, birthDate, employed, occupation, addressId) { + from(person) + where { id isLessThan 5 } + and { + id isLessThan 4 + and { + id isLessThan 3 + or { id isLessThan 2 } + } + } + orderBy(id) + limit(3) +} +``` + +These methods create models or providers depending on which package is used: + +| Package | Resulting Object | +|----------------------------------------------|---------------------------------------------------------------------------------------| +| org.mybatis.dynamic.sql.util.kotlin.model | org.mybatis.dynamic.sql.select.SelectModel | +| org.mybatis.dynamic.sql.util.kotlin.mybatis3 | org.mybatis.dynamic.sql.select.render.SelectStatementProvider (rendered for MyBatis3) | +| org.mybatis.dynamic.sql.util.kotlin.spring | org.mybatis.dynamic.sql.select.render.SelectStatementProvider (rendered for Spring) | + +## Multi-Select Statement + +A multi-select statement is a special case of a union query. In a multi-select statement, each select statement is +wrapped with parentheses. This means that you can use "order by" and paging clauses on the select statements that are +merged with a "union" or "union all" operator. You can also apply "order by" and paging clauses to the query as a whole. + +The full DSL for multi-select statements looks like this: + +```kotlin +val selectStatement = multiSelect { + selectDistinct(id, firstName, lastName, birthDate, employed, occupation, addressId) { + from(person) + where { id isLessThanOrEqualTo 2 } + orderBy(id) + limit(1) + } + unionAll { + select(id, firstName, lastName, birthDate, employed, occupation, addressId) { + from(person) + where { id isGreaterThanOrEqualTo 4 } + orderBy(id.descending()) + limit(1) + } + } + orderBy(id) + fetchFirst(1) + offset(1) +} +``` + +Each nested select statement can be either "select" or "selectDistinct". They can be merged with either +"union" or "unionAll". There is no limit to the number of statements that can be merged. + +These methods create models or providers depending on which package is used: + +| Package | Resulting Object | +|----------------------------------------------|---------------------------------------------------------------------------------------| +| org.mybatis.dynamic.sql.util.kotlin.model | org.mybatis.dynamic.sql.select.MultiSelectModel | +| org.mybatis.dynamic.sql.util.kotlin.mybatis3 | org.mybatis.dynamic.sql.select.render.SelectStatementProvider (rendered for MyBatis3) | +| org.mybatis.dynamic.sql.util.kotlin.spring | org.mybatis.dynamic.sql.select.render.SelectStatementProvider (rendered for Spring) | + +## Update Statement + +Update statement support enables the creation of methods that execute an update allowing a user to specify SET clauses +and where clauses. + +The DSL for update statements looks like this: + +```kotlin +val updateStatement = update(person) { + set(firstName).equalTo("Sam") + where { firstName isEqualTo "Fred" } +} +``` + +If you omit the `where` clause, the statement will update every row in a table. + +This method creates models or providers depending on which package is used: + +| Package | Resulting Object | +|----------------------------------------------|---------------------------------------------------------------------------------------| +| org.mybatis.dynamic.sql.util.kotlin.model | org.mybatis.dynamic.sql.update.UpdateModel | +| org.mybatis.dynamic.sql.util.kotlin.mybatis3 | org.mybatis.dynamic.sql.update.render.UpdateStatementProvider (rendered for MyBatis3) | +| org.mybatis.dynamic.sql.util.kotlin.spring | org.mybatis.dynamic.sql.update.render.UpdateStatementProvider (rendered for Spring) | diff --git a/src/site/markdown/docs/kotlinSpring.md b/src/site/markdown/docs/kotlinSpring.md index 25b6567fa..ec008c2c1 100644 --- a/src/site/markdown/docs/kotlinSpring.md +++ b/src/site/markdown/docs/kotlinSpring.md @@ -1,291 +1,622 @@ # Kotlin Support for Spring -MyBatis Dynamic SQL includes Kotlin extension methods that enable an SQL DSL for Kotlin. This is the recommended method of using the library in Kotlin with Spring JDBC template. +MyBatis Dynamic SQL includes Kotlin extensions for Spring that simplify execution of statements generated by the library. +The library will render SQL in a format that is suitable for use with Spring's named parameter JDBC template. The only +difficulty with using the DSL directly is that the parameters for statements need to be formatted properly for Spring. +For example, this may involve the use of a `BeanPropertySqlParameterSource` or a `MapSqlParameterSource` depending on +the statement type. The Kotlin DSL hides all these details. -This page will show our recommended pattern for using the MyBatis Dynamic SQL with Kotlin and Spring JDBC Template. The code shown on this page is from the `src/test/kotlin/examples/kotlin/spring/canonical` directory in this repository. That directory contains a complete example of using this library with Kotlin and Spring. +The Spring extensions also allow use of Spring's row mappers for ResultSets, and generated key holder for retrieving +generated keys on certain insert statements. + +This page will show our recommended pattern for using the MyBatis Dynamic SQL with Kotlin and Spring JDBC Template. +The code shown on this page is from the `src/test/kotlin/examples/kotlin/spring/canonical` directory in this repository. +That directory contains a complete example of using this library with Kotlin and Spring. All Kotlin support for Spring is available in two packages: -* `org.mybatis.dynamic.sql.util.kotlin` - contains extension methods and utilities to enable an idiomatic Kotlin DSL for MyBatis Dynamic SQL. These objects can be used for clients using any execution target (i.e. MyBatis3 or Spring JDBC Templates) -* `org.mybatis.dynamic.sql.util.kotlin.spring` - contains utlities specifically to simplify integration with Spring JDBC Template +* `org.mybatis.dynamic.sql.util.kotlin.elements` - contains the basic DSL elements common to all runtimes +* `org.mybatis.dynamic.sql.util.kotlin.spring` - contains utilities that simplify integration with Spring JDBC Template + +The Kotlin support for Spring JDBC is implemented as extension methods to `NamedParameterJdbcTemplate`. There are extension +methods to support count, delete, insert, select, and update operations based on SQL generated by this library. +For each operation, there are two different methods of executing SQL: -The Kotlin support for Spring is implemented as extension methods to `NamedParameterJdbcTemplate`. There are extension methods to support count, delete, insert, select, and update operations based on SQL generated by this library. For each operation, there are two different methods of executing SQL. With the first method you build the appropriate SQL provider object as a separate step before executing the SQL. The second method combines these two operations into a single step. We will illustrate both approaches below. +1. The first method is a two-step method. With this method you build SQL provider objects as shown on the Kotlin + overview page and then execute the generated SQL by passing the provider to an extension method + on `NamedParameterJdbcTemplate` +2. The second method is a one-step method that combines these operations into a single step + +We will illustrate both approaches below. ## Kotlin Dynamic SQL Support Objects -Because Kotlin does not support static class members, we recommend a simpler pattern for creating the class containing the support objects. For example: + +The pattern for the metamodel is the same as shown on the Kotlin overview page. We'll repeat it here to show some +specifics for Spring. ```kotlin +import org.mybatis.dynamic.sql.AlisableSqlTable +import org.mybatis.dynamic.sql.util.kotlin.elements.column +import java.util.Date + object PersonDynamicSqlSupport { - object Person : SqlTable("Person") { - val id = column("id", JDBCType.INTEGER) - val firstName = column("first_name", JDBCType.VARCHAR) - val lastName = column("last_name", JDBCType.VARCHAR) - val birthDate = column("birth_date", JDBCType.DATE) - val employed = column("employed", JDBCType.VARCHAR) - val occupation = column("occupation", JDBCType.VARCHAR) - val addressId = column("address_id", JDBCType.INTEGER) + val person = Person() + val id = person.id + val firstName = person.firstName + val lastName = person.lastName + val birthDate = person.birthDate + val employed = person.employed + val occupation = person.occupation + val addressId = person.addressId + + class Person : AlisableSqlTable("Person", ::Person) { + val id = column(name = "id") + val firstName = column(name = "first_name") + val lastName = column( + name = "last_name", + parameterTypeConverter = lastNameConverter + ) + val birthDate = column(name = "birth_date") + val employed = column( + name = "employed", + parameterTypeConverter = booleanToStringConverter + ) + val occupation = column(name = "occupation") + val addressId = column(name = "address_id") } } ``` -This object is a singleton containing the `SqlTable` and `SqlColumn` objects that map to the database table. +Note the use of a "parameter type converter" on the `employed` column. This allows us to use the column as a Boolean in +Kotlin, but store the values "Yes" or "No" on the database. The type converter looks like this: + +```kotlin +val booleanToStringConverter: (Boolean?) -> String = { it?.let { if (it) "Yes" else "No" } ?: "No" } +``` + +The type converter will be used on general insert statements, update statements, and where clauses. It is not used on +insert statements that map insert fields to properties in a data class. So you will need to add properties to a data +class to use in that case. In the examples below, you will see use of a data class property `employedAsString`. +This can easily be implemented by reusing the converter function as shown below... -**Important Note:** Spring JDBC template does not support type handlers, so column definitions in the support class should match the data types of the corresponding column. - -## Count Method Support +```kotlin +data class PersonRecord( + ... + var employed: Boolean? = null, + ... +) { + val employedAsString: String + get() = booleanToStringConverter(employed) +} +``` -A count query is a specialized select - it returns a single column - typically a long - and supports joins and a where clause. +## Count Statements -The DSL for count methods looks like this: +### Two-Step Method +Count statements are constructed as shown on the Kotlin overview page. These methods create a +`SelectStatementProvider` that can be executed with an extension method for `NamedParameterJdbcTemplate` like this: ```kotlin - val countStatement = countFrom(Person) { // countStatement is a SelectStatementProvider - where(id, isLessThan(4)) - } +import org.mybatis.dynamic.sql.util.kotlin.spring.count + +val countStatement = count(...) // not shown... see the overview page for examples +val template: NamedParameterJdbcTemplate = getTemplate() // not shown +val rows = template.count(countStatement) // rows is a Long ``` -This code creates a `SelectStatementProvider` that can be executed with an extension method for `NamedParameterJdbcTemplate` like this: +### One-Step Method +Count statements can be constructed and executed in a single step with code like the following: ```kotlin - val template: NamedParameterJdbcTemplate = getTemplate() // not shown - val rows = template.count(countStatement) // rows is a Long +import org.mybatis.dynamic.sql.util.kotlin.spring.count +import org.mybatis.dynamic.sql.util.kotlin.spring.countDistinct +import org.mybatis.dynamic.sql.util.kotlin.spring.countFrom + +val template: NamedParameterJdbcTemplate = getTemplate() // not shown + +val rowcount = template.countFrom(person) { + where { id isLessThan 4 } +} + +val columnCount = template.count(lastName) { + from(person) + where { id isLessThan 4 } +} + +val distinctColumnCount = template.countDistinct(lastName) { + from(person) + where { id isLessThan 4 } +} ``` -This is the two step execution process. This can be combined into a single step with code like the following: +There is also a method that can be used to count all rows in a table: ```kotlin - val rows = template.countFrom(Person) { - where(id, isLessThan(4)) - } +val rows = template.countFrom(Person) { + allRows() +} ``` -There is also an extention method that can be used to count all rows in a table: +## Delete Statement + +### Two-Step Method +Delete statements are constructed as shown on the Kotlin overview page. These methods create a +`DeleteStatementProvider` that can be executed with an extension method for `NamedParameterJdbcTemplate` like this: ```kotlin - val rows = template.countFrom(Person) { - allRows() - } +import org.mybatis.dynamic.sql.util.kotlin.spring.deleteFrom + +val deleteStatement = deleteFrom(...) // not shown... see the overview page for examples +val template: NamedParameterJdbcTemplate = getTemplate() // not shown +val rows = template.delete(deleteStatement) // rows is an Int ``` -## Delete Method Support +### One-Step Method +Delete statements can be constructed and executed in a single step with code like the following: + +```kotlin +import org.mybatis.dynamic.sql.util.kotlin.spring.deleteFrom -Delete method support enables the creation of methods that execute a delete statement allowing a user to specify a where clause at runtime, but abstracting away all other details. +val template: NamedParameterJdbcTemplate = getTemplate() // not shown + +val rows = template.deleteFrom(person) { + where { id isLessThan 4 } +} +``` -The DSL for delete methods looks like this: +There is also a method that can be used to count all rows in a table: ```kotlin - val deleteStatement = deleteFrom(Person) { // deleteStatement is a DeleteStatementProvider - where(id, isLessThan(4)) - } +val rows = template.deleteFrom(person) { + allRows() +} ``` -This code creates a `DeleteStatementProvider` that can be executed with an extension method for `NamedParameterJdbcTemplate` like this: +## Single Row Insert Statement + +### Two-Step Method +Single record insert statements are constructed as shown on the Kotlin overview page. These methods create a +`InsertStatementProvider` that can be executed with an extension method for `NamedParameterJdbcTemplate` like this: ```kotlin - val template: NamedParameterJdbcTemplate = getTemplate() // not shown - val rows = template.delete(deleteStatement) // rows is an Int +val insertStatement = insert(...) // not shown, see the overview page for examples +val template: NamedParameterJdbcTemplate = getTemplate() // not shown +val rows = template.insert(insertStatement) // rows is an Int ``` -This is the two step execution process. This can be combined into a single step with code like the following: +If you want to retrieve generated keys, you can use Spring's KeyHolder as follows: ```kotlin - val rows = template.deleteFrom(Person) { - where(id, isLessThan(4)) - } +val keyHolder = GeneratedKeyHolder() +val rows = template.insert(insertStatement, keyHolder) // rows is an Int ``` -There is also an extention method that can be used to count all rows in a table: +### One-Step Method +Single record insert statements can be constructed and executed in a single step with code like the following: ```kotlin - val rows = template.deleteFrom(Person) { - allRows() +val row = PersonRecord(100, "Joe", "Jones", Date(), true, "Developer", 1) + +val rows = template.insert(row) { + into(Person) + map(id) toProperty "id" + map(firstName) toProperty "firstName" + map(lastName) toProperty "lastName" + map(birthDate) toProperty "birthDate" + map(employed) toProperty "employedAsString" + map(occupation).toPropertyWhenPresent("occupation", row::occupation) + map(addressId) toProperty "addressId" +} +``` + +Note the use of the `toPropertyWhenPresent` mapping - this will only set the insert value if the value of the property +is non-null. Also note that you can use the mapping methods to map insert fields to nulls and constants if desired. + +Using a KeyHolder with the single step method looks like this: + +```kotlin +val keyHolder = GeneratedKeyHolder() +val row = PersonRecord(100, "Joe", "Jones", Date(), true, "Developer", 1) + +val rows = template.withKeyHolder(keyHolder) { + insert(row) { + into(Person) + map(id) toProperty"id" + map(firstName) toProperty "firstName" + map(lastName) toProperty "lastName" + map(birthDate) toProperty "birthDate" + map(employed) toProperty "employedAsString" + map(occupation).toPropertyWhenPresent("occupation", row::occupation) + map(addressId) toProperty "addressId" } +} +``` + +## General Insert Statement + +### Two-Step Method +General insert statements are constructed as shown on the Kotlin overview page. These methods create a +`GeneralInsertStatementProvider` that can be executed with an extension method for `NamedParameterJdbcTemplate` like this: + +```kotlin +val insertStatement = insertInto(...) // not shown... see overview page for examples +val template: NamedParameterJdbcTemplate = getTemplate() // not shown +val rows = template.generalInsert(insertStatement) // rows is an Int ``` -## Insert Method Support +If you want to retrieve generated keys, you can use Spring's KeyHolder as follows: -Insert method support enables the creation of arbitrary insert statements. +```kotlin +val keyHolder = GeneratedKeyHolder() +val rows = template.generalInsert(insertStatement, keyHolder) // rows is an Int +``` -The DSL for insert methods looks like this: +### One-Step Method +General insert statements can be constructed and executed in a single step with code like the following: ```kotlin - val record = PersonRecord(100, "Joe", "Jones", Date(), "Yes", "Developer", 1) +val myOccupation = "Developer" + +val rows = template.insertInto(Person) { + set(id) toValue 100 + set(firstName) toValue "Joe" + set(lastName) toValue "Jones" + set(birthDate) toValue Date() + set(employed) toValue true + set(occupation) toValueWhenPresent myOccupation + set(addressId) toValue 1 +} +``` + +Note the use of the `toValueWhenPresent` mapping - this will only set the insert value if the value of the property +is non-null. Also note that you can use the mapping methods to map insert fields to nulls and constants if desired. - val insertStatement = insert(record).into(Person) { // insertStatement is an InsertStatementProvider - map(id).toProperty("id") - map(firstName).toProperty("firstName") - map(lastName).toProperty("lastName") - map(birthDate).toProperty("birthDate") - map(employed).toProperty("employed") - map(occupation).toProperty("occupation") - map(addressId).toProperty("addressId") +Using a KeyHolder with the single step method looks like this: + +```kotlin +val keyHolder = GeneratedKeyHolder() +val myOccupation = "Developer" + +val rows = template.withKeyHolder(keyHolder) { + insertInto(Person) { + set(id) toValue 100 + set(firstName) toValue "Joe" + set(lastName) toValue "Jones" + set(birthDate) toValue Date() + set(employed) toValue true + set(occupation) toValueWhenPresent myOccupation + set(addressId) toValue 1 } +} ``` -This code creates an `InsertStatementProvider` that can be executed with an extension method for `NamedParameterJdbcTemplate` like this: +## Multi-Row Insert Statement + +### Two-Step Method +Multi-Row insert statements are constructed as shown on the Kotlin overview page. These methods create a +`MultiRowInsertStatementProvider` that can be executed with an extension method for `NamedParameterJdbcTemplate` like this: ```kotlin - val template: NamedParameterJdbcTemplate = getTemplate() // not shown - val rows = template.insert(insertStatement) // rows is an Int +val insertStatement = insertMultiple(...) // not shown... see overview page for examples +val template: NamedParameterJdbcTemplate = getTemplate() // not shown +val rows = template.insertMultiple(insertStatement) // rows is an Int ``` -This is the two step execution process. This can be combined into a single step with code like the following: +If you want to retrieve generated keys, you can use Spring's KeyHolder as follows: ```kotlin - val record = PersonRecord(100, "Joe", "Jones", Date(), "Yes", "Developer", 1) +val keyHolder = GeneratedKeyHolder() +val rows = template.insertMultiple(insertStatement, keyHolder) // rows is an Int +``` + +### One-Step Method +Multi-Row insert statements can be constructed and executed in a single step with code like the following: + +```kotlin +val record1 = PersonRecord(100, "Joe", LastName("Jones"), Date(), true, "Developer", 1) +val record2 = PersonRecord(101, "Sarah", LastName("Smith"), Date(), true, "Architect", 2) + +val rows = template.insertMultiple(record1, record2) { + into(Person) + map(id) toProperty "id" + map(firstName) toProperty "firstName" + map(lastName) toProperty "lastNameAsString" + map(birthDate) toProperty "birthDate" + map(employed) toProperty "employedAsString" + map(occupation) toProperty "occupation" + map(addressId) toProperty "addressId" +} +``` + +Using a KeyHolder with the single step method looks like this: - val rows = template.insert(record, Person) { - map(id).toProperty("id") - map(firstName).toProperty("firstName") - map(lastName).toProperty("lastName") - map(birthDate).toProperty("birthDate") - map(employed).toProperty("employed") - map(occupation).toPropertyWhenPresent("occupation", record::occupation) - map(addressId).toProperty("addressId") +```kotlin +val keyHolder = GeneratedKeyHolder() +val record1 = PersonRecord(100, "Joe", LastName("Jones"), Date(), true, "Developer", 1) +val record2 = PersonRecord(101, "Sarah", LastName("Smith"), Date(), true, "Architect", 2) + +val rows = template.withKeyHolder(keyHolder) { + insertMultiple(record1, record2) { + into(Person) + map(id) toProperty "id" + map(firstName) toProperty "firstName" + map(lastName) toProperty "lastNameAsString" + map(birthDate) toProperty "birthDate" + map(employed) toProperty "employedAsString" + map(occupation) toProperty "occupation" + map(addressId) toProperty "addressId" } +} +``` + +## Batch Insert Statement + +### Two-Step Method +Batch insert statements are constructed as shown on the Kotlin overview page. These methods create a +`BatchInsert` that can be executed with an extension method for `NamedParameterJdbcTemplate` like this: + +```kotlin +val insertStatement = insertBatch(...) // not shown... see overview page for examples +val template: NamedParameterJdbcTemplate = getTemplate() // not shown +val rows = template.insertBatch(insertStatement) // rows is an IntArray ``` -Note the use of the `toPropertyWhenPresent` mapping - this will only set the insert value if the value of the property is non null. Also note that you can use the mapping methods to map insert fields to nulls and constants if desired. +Spring does not support retrieval of generated keys with batch insert statements. + +### One-Step Method +Batch statements can be constructed and executed in a single step with code like the following: -## Select Method Support +```kotlin +val record1 = PersonRecord(100, "Joe", LastName("Jones"), Date(), true, "Developer", 1) +val record2 = PersonRecord(101, "Sarah", LastName("Smith"), Date(), true, "Architect", 2) + +val rows = template.insertBatch(record1, record2) { + into(Person) + map(id) toProperty "id" + map(firstName) toProperty "firstName" + map(lastName) toProperty "lastNameAsString" + map(birthDate) toProperty "birthDate" + map(employed) toProperty "employedAsString" + map(occupation) toProperty "occupation" + map(addressId) toProperty "addressId" +} +``` -Select method support enables the creation of methods that execute a query allowing a user to specify a where clause and/or an order by clause and/or pagination clauses at runtime, but abstracting away all other details. +## Insert Select Statement -The DSL for select methods looks like this: +### Two-Step Method +Insert select statements are constructed as shown on the Kotlin overview page. These methods create a +`InsertSelectStatementProvider` that can be executed with an extension method for `NamedParameterJdbcTemplate` like this: ```kotlin - val selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, // selectStatement is a SelectStatementProvider - addressId).from(Person) { - where(id, isLessThan(5)) - and(id, isLessThan(4)) { - and(id, isLessThan(3)) { - and(id, isLessThan(2)) - } - } - orderBy(id) - limit(3) +val insertStatement = insertSelect(...) // not shown... see overview page for examples +val template: NamedParameterJdbcTemplate = getTemplate() // not shown +val rows = template.insertSelect(insertStatement) // rows is an Int +``` + +If you want to retrieve generated keys, you can use Spring's KeyHolder as follows: + +```kotlin +val keyHolder = GeneratedKeyHolder() +val rows = template.insertSelect(insertStatement, keyHolder) // rows is an Int +``` + +### One-Step Method +Insert select statements can be constructed and executed in a single step with code like the following: + +```kotlin +val insertSelectRows: Int = template.insertSelect(person) { + columns(id, firstName, lastName, birthDate, employed, occupation, addressId) + select( + add(id, constant("100")), firstName, lastName, birthDate, employed, occupation, addressId + ) { + from(person) + where { employed.isTrue() } } +} ``` -This code creates a `SelectStatementProvider` that can be executed with an extension method for `NamedParameterJdbcTemplate` like this: +Using a KeyHolder with the single step method looks like this: ```kotlin - val template: NamedParameterJdbcTemplate = getTemplate() // not shown - val rows = template.selectList(selectStatement) { rs, _ -> // rows is a List of PersonRecord in this case - val record = PersonRecord() - record.id = rs.getInt(1) - record.firstName = rs.getString(2) - record.lastName = rs.getString(3) - record.birthDate = rs.getTimestamp(4) - record.employed = rs.getString(5) - record.occupation = rs.getString(6) - record.addressId = rs.getInt(7) - record +val keyHolder = GeneratedKeyHolder() + +val rows = template.withKeyHolder(keyHolder) { + insertSelect(person) { + columns(id, firstName, lastName, birthDate, employed, occupation, addressId) + select( + add(id, constant("100")), firstName, lastName, birthDate, employed, occupation, addressId + ) { + from(person) + where { employed.isTrue() } + } } +} ``` -Note that you must provide a row mapper to tell Spring JDBC how to create result objects. - -This is the two step execution process. This can be combined into a single step with code like the following: - -```kotlin - val rows = template.select(id, firstName, lastName, birthDate, employed, occupation, addressId) - .from(Person) { - where(id, isLessThan(4)) { - and(occupation, isNotNull()) - } - and(occupation, isNotNull()) - orderBy(id) - limit(3) - }.withRowMapper { rs, _ -> - val record = PersonRecord() - record.id = rs.getInt(1) - record.firstName = rs.getString(2) - record.lastName = rs.getString(3) - record.birthDate = rs.getTimestamp(4) - record.employed = rs.getString(5) - record.occupation = rs.getString(6) - record.addressId = rs.getInt(7) - record - } -``` - -There are similar methods for selecing a single row, or executing a select distinct query. For example, you could implement a "select by primary key" method using code like this: - -```kotlin - val record = template.selectOne(id, firstName, lastName, birthDate, employed, occupation, addressId) - .from(Person) { - where(id, isEqualTo(key)) - }.withRowMapper { rs, _ -> - val record = PersonRecord() - record.id = rs.getInt(1) - record.firstName = rs.getString(2) - record.lastName = rs.getString(3) - record.birthDate = rs.getTimestamp(4) - record.employed = rs.getString(5) - record.occupation = rs.getString(6) - record.addressId = rs.getInt(7) - record - } -``` - -In this case, the data type for `record` would be `PersonRecord?` - a nullable value. - -There is also an extention method that can be used to select all rows in a table: - -```kotlin - val rows = template.select(id, firstName, lastName, birthDate, employed, occupation, addressId) - .from(Person) { - allRows() - orderBy(id) - }.withRowMapper { rs, _ -> - val record = PersonRecord() - record.id = rs.getInt(1) - record.firstName = rs.getString(2) - record.lastName = rs.getString(3) - record.birthDate = rs.getTimestamp(4) - record.employed = rs.getString(5) - record.occupation = rs.getString(6) - record.addressId = rs.getInt(7) - record - } -``` - -Note that we have supplied an `order by` clause as well. -## Update Method Support +## Select Statement -Update method support enables the creation of methods that execute an update allowing a user to specify SET clauses and/or a WHERE clause, but abstracting away all other details. +### Spring Row Mappers +There are several ways to execute select statements with Spring. Many of the methods require the use of a row mapper. +A row mapper is a user provided function that creates objects based on the values in a `ResultSet`. +The `rowMapper` function will be called repeatedly until the end of the result set is reached. The function accepts two +parameters - the `ResultSet` and an `Int` which will be set to the current row number. Many times the row number is +ignored. -The DSL for delete methods looks like this: +In Kotlin, you can declare the row mapper function either as a declared function, or as a function variable. A declared +function could look like the following: ```kotlin - val updateStatement = update(Person) { // updateStatement is an UpdateStatementProvider - set(firstName).equalTo("Sam") - where(firstName, isEqualTo("Fred")) - } +import java.sql.ResultSet + +fun rowMapper(resultSet: ResultSet, rowNum: Int): PersonRecord = + PersonRecord( + id = rs.getInt(id.name()), + firstName = rs.getString(firstName.name()), + lastName = rs.getString(lastName.name()), + birthDate = rs.getDate(birthDate.name()), + employed = rs.getString(employed.name()) == "Yes", + occupation = rs.getString(occupation.name()), + addressId = rs.getInt(addressId.name()) + ) ``` -This code creates an `UpdateStatementProvider` that can be executed with an extension method for `NamedParameterJdbcTemplate` like this: +A function variable could look like this: ```kotlin - val template: NamedParameterJdbcTemplate = getTemplate() // not shown - val rows = template.update(updateStatement) // rows is an Int +import java.sql.ResultSet + +val rowMapper: (ResultSet, Int) -> PersonRecord = { rs, _ -> + PersonRecord( + id = rs.getInt(id.name()), + firstName = rs.getString(firstName.name()), + lastName = rs.getString(lastName.name()), + birthDate = rs.getDate(birthDate.name()), + employed = rs.getString(employed.name()) == "Yes", + occupation = rs.getString(occupation.name()), + addressId = rs.getInt(addressId.name()) + ) +} ``` -This is the two step execution process. This can be combined into a single step with code like the following: +Note that in this case we are ignoring the row number. You can also pass a function like this directly to the extension +methods as a lambda as we will see below. + +### Two-Step Method +Select statements are constructed as shown on the Kotlin overview page. These methods create a +`SelectStatementProvider` that can be executed with extension methods for `NamedParameterJdbcTemplate`. There are several +extension methods that can be used in the two-step method as detailed below: + +| Method | Comments| +|---|---| +| selectList(SelectStatementProvider, RowMapper) | Executes a select statement and returns a list (the list will be empty if no records match). The row mapper is used to map result sets for rows. | +| selectList(SelectStatementProvider, KClass) | Executes a select statement and returns a list (the list will be empty if no records match). This method can be used to execute a select statement that returns a single column. Spring will attempt to retrieve objects of type `KClass` from the result set. | +| selectOne(SelectStatementProvider, RowMapper) | Executes a select statement and returns a single object (or null if no records match). The row mapper is used to map result sets for row. | +| selectOne(SelectStatementProvider, KClass) | Executes a select statement and returns a single object (or null if no records match). This method can be used to execute a select statement that returns a single column. Spring will attempt to retrieve an object of type `KClass` from the result set. | + +The following example shows the most common case: executing a statement that returns multiple rows with a user provided +row mapper. The row mapper is passed as a lambda and ignores the row number: ```kotlin - val rows = template.update(Person) { - set(firstName).equalTo("Sam") - where(firstName, isEqualTo("Fred")) +val selectStatement = select(...) // not shown... see overview page for examples +val template: NamedParameterJdbcTemplate = getTemplate() // not shown +val rows = template.selectList(selectStatement) { rs, _ -> // rows is a List of PersonRecord in this case + PersonRecord( + id = rs.getInt(id.name()), + firstName = rs.getString(firstName.name()), + lastName = rs.getString(lastName.name()), + birthDate = rs.getDate(birthDate.name()), + employed = rs.getString(employed.name()) == "Yes", + occupation = rs.getString(occupation.name()), + addressId = rs.getInt(addressId.name()) + ) +} +``` + +### One-Step Method +Select statements can be constructed and executed in a single step with code like the following: + +```kotlin +val personRecords: List = template.select(id, firstName, lastName, birthDate, employed, occupation, addressId) { + from(person) + where { id isLessThan 5 } + and { + id isLessThan 4 + and { + id isLessThan 3 + or { id isLessThan 2 } + } } + orderBy(id) + limit(3) +}.withRowMapper { rs, _ -> + PersonRecord( + id = rs.getInt(id.name()), + firstName = rs.getString(firstName.name()), + lastName = rs.getString(lastName.name()), + birthDate = rs.getDate(birthDate.name()), + employed = rs.getString(employed.name()) == "Yes", + occupation = rs.getString(occupation.name()), + addressId = rs.getInt(addressId.name()) + ) +} +``` + +There are similar methods for selecting a single row, or executing a select distinct query. A single row select looks +like this: + +```kotlin +val personRecord: PersonRecord? = template.selectOne(id, firstName, lastName, birthDate, employed, occupation, addressId) { + from(Person) + where { id isEqualTo key } +}.withRowMapper { rs, _ -> + PersonRecord( + id = rs.getInt(id.name()), + firstName = rs.getString(firstName.name()), + lastName = rs.getString(lastName.name()), + birthDate = rs.getDate(birthDate.name()), + employed = rs.getString(employed.name()) == "Yes", + occupation = rs.getString(occupation.name()), + addressId = rs.getInt(addressId.name()) + ) +} +``` + +A distinct query looks like this: + +```kotlin +val personRecord: List = template.selectDistinct(id, firstName, lastName, birthDate, employed, occupation, addressId) { + from(Person) + where { id isLessThan key } +}.withRowMapper { rs, _ -> + PersonRecord( + id = rs.getInt(id.name()), + firstName = rs.getString(firstName.name()), + lastName = rs.getString(lastName.name()), + birthDate = rs.getDate(birthDate.name()), + employed = rs.getString(employed.name()) == "Yes", + occupation = rs.getString(occupation.name()), + addressId = rs.getInt(addressId.name()) + ) +} +``` + +## Multi-Select Statement Support + +Multi-select statements are a special case of select statement. All the above information about row mappers applies +equally to multi-select statements. + +The library does not provide a "one-step" shortcut for multi-select queries. You can execute a multi-select query +with the two-step method using either the "selectList" or "selectOne" extension methods as shown above. + +## Update Method Support + +### Two-Step Method +Update statements are constructed as shown on the Kotlin overview page. These methods create an +`UpdateStatementProvider` that can be executed with an extension method for `NamedParameterJdbcTemplate` like this: + +```kotlin +val updateStatement = update(...) // not shown... see overview page for examples +val template: NamedParameterJdbcTemplate = getTemplate() // not shown +val rows = template.update(updateStatement) // rows is an Int ``` -There a many different set mappings the allow setting values to null, constants, etc. There is also a mapping that will only set the column value if the passed value is non null. +### One-Step Method +Update statements can be constructed and executed in a single step with code like the following: + +```kotlin +val rows = template.update(Person) { + set(firstName).equalTo("Sam") + where { firstName isEqualTo "Fred" } +} +``` + +There a many set mappings that allow setting values to null, constants, etc. There is also a mapping that will only set +the column value if the passed value is non-null. If you wish to update all rows in a table, simply omit the where clause: ```kotlin - val rows = template.update(Person) { - set(firstName).equalTo("Sam") - } +val rows = template.update(Person) { + set(firstName).equalTo("Sam") +} ``` diff --git a/src/site/markdown/docs/kotlinWhereClauses.md b/src/site/markdown/docs/kotlinWhereClauses.md new file mode 100644 index 000000000..acb20521e --- /dev/null +++ b/src/site/markdown/docs/kotlinWhereClauses.md @@ -0,0 +1,371 @@ +# Kotlin Where Clauses + +Where clauses can be supplied to delete, select, and update statements. The Kotlin DSL provides an implementation +of a where clause that looks very close to natural SQL. This is accomplished through a combination of operator +overload functions, infix functions, and Kotlin receiver functions. + +## Simple Where Clauses + +The simplest form of where clause includes a single condition. See the following examples: + +```kotlin +select(foo) { + from(bar) + where { id isEqualTo 3 } +} + +select(foo) { + from(bar) + where { id isBetween 3 and 7 } +} +``` + +In this case, `id` is an SqlColumn of type Integer, `isEqualTo` and `isBetween` are infix functions. These clauses can +also be written as follows by explicitly calling each function: + +```kotlin +select(foo) { + from(bar) + where { id.isEqualTo(3) } +} + +select(foo) { + from(bar) + where { id.isBetween(3).and(7) } +} +``` + +Most, but not all, of the built-in conditions can be expressed as infix functions. Conditions without parameters, +or varargs conditions, cannot be called as an infix function. Good examples of conditions that cannot be called via +an infix function are `isNull` and `isIn`. In those cases you will need to call the function directly as follows: + +```kotlin +select(foo) { + from(bar) + where { id.isNull() } +} + +select(foo) { + from(bar) + where { id.isIn(1, 2, 3) } +} +``` + +## Using Filter and Map + +Many conditions support `filter` and `map` functions that can be used to test whether the condition should be rendered +or to change the value of the condition parameter(s). If you need to use the `filter` or `map` functions, then you +cannot use the infix functions. In this case you can use a function that creates the condition and then apply +that condition to the where clause via the invoke operator. For example: + +```kotlin +select(foo) { + from(bar) + where { firstName (isLike("fred").map { "%$it%" }) } // add wildcards for like +} +``` + +In this case, `isLike` is a function in the `org.mybatis.dynamic.sql.util.kotlin.elements` package, not the infix +function. Note also that the condition is enclosed in parentheses. This is actually a function call using a Kotlin +invoke operator overload. This can also be called explicitly without the operator overload as follows: + +```kotlin +select(foo) { + from(bar) + where { firstName.invoke(isLike("fred").map { "%$it%" }) } // add wildcards for like +} +``` + +## Compound Where Clauses + +Of course many where clauses are composed of more than one condition. The where DSL supports arbitrarily complex +where clauses with and/or/not phrases. See the following example of a complex where clause: + +```kotlin +select(foo) { + from(bar) + where { + id isEqualTo 3 + or { id isEqualTo 4 } + and { not { id isEqualTo 6 } } + } +} +``` + +The `and`, `or`, and `not` functions each create a new context that can in turn include `and`, `or`, and `not` +functions. The DSL has no practical limit on the depth of nesting of these functions. When there are nested +`and` and `or` functions, the curly braces will be rendered as parentheses in the final SQL if the context contains +more than one condition. + +## Initial and Subsequent Conditions + +As shown above, the `where`, `and`, `or`, `not`, and `group` functions create a context where conditions can be +specified (`group` is detailed below). Every context supports two types of conditions: + +1. A single initial condition (like `id isEqualTo 3`). If you specify more than one initial condition, the library + will throw a runtime exception. There are multiple types of initial conditions detailed below. +2. Any number of subsequent conditions created by the `and` or `or` functions + +Everything is optional - if you don't specify an initial condition, or any subsequent conditions, then nothing will +render. + +For each context, the renderer will add parenthesis around the rendered context if there is more than one condition in +the context. Remember that a `filter` function can be used to remove some conditions from rendering, so the +parentheses are added only if there is more than one condition that renders. + +If you neglect to specify an initial condition and only specify `and` and `or` groups, then the first "and" or "or" +will be removed during rendering. This to avoid a rendered where clause like "where and id = 3". This can be useful +in situations where a where clause is composed by a number of different functions - there is no need to keep track +of who goes first as the renderer will automatically strip the first connector. + +### Initial Condition Types + +There are four types of initial conditions. Only one of the initial condition types may be specified in any +given context. Others must be enclosed in an `and` or an `or` block. The four types are as follows: + +1. **Column and Criterion** - either with the infix functions, or the invoke function as shown above +2. **Not** - appends "not" to a group of criteria or a single criterion as shown above +3. **Exists** - for executing an exists sub-query: + + ```kotlin + select(foo) { + from(bar) + where { + exists { + select(foo.allColumns()) + from(foo) + where { foo.id isEqualTo bar.fooId } + } + } + } + ``` + + You can accomplish a "not exists" by nesting `exists` inside a `not` block: + + ```kotlin + select(foo) { + from(bar) + where { not { + exists { + select(foo.allColumns()) + from(foo) + where { foo.id isEqualTo bar.fooId } + } + }} + } + ``` + +4. **Group** - for grouping conditions with parentheses: + + ```kotlin + select(foo) { + from(bar) + where { + group { + id isEqualTo 3 + and { id isEqualTo 4 } + } + or { firstName.isNull() } + } + } + ``` + + The `group` function is used to insert parentheses around a group of conditions before + and `and` or an `or`. + +## Extending Where Clauses + +In addition to the built-in conditions supplied with the library, it is also possible to write your own custom +conditions. Any custom condition can be used with the "invoke operator" method shown above in the +"Using Filter And Map" section above. + +At this time, it is not possible to add infix functions for custom conditions to the library. This is due to an +underlying limitation in Kotlin itself. There is a Kotlin language enhancement on the roadmap that will likely +remove this limitation. That enhancement will allow multiple receivers for an extension function. You can follow +progress of that enhancement here: https://youtrack.jetbrains.com/issue/KT-42435 + +## Migrating from Prior Releases + +In version 1.4.0 the where DSL improved significantly and is now implemented as shown on this page. Many methods from +previous releases are now deprecated. One of the primary motivations for this change was that compound criteria +from prior releases were difficult to reason about - the Kotlin syntax was very different from the generated SQL. +In complex where clauses, the code could become very difficult to understand. + +With the updated DSL, the Kotlin code is much closer to the generated SQL and there is a consistent use of curly braces +to denote where parentheses should be generated in SQL. + +This section will detail the patterns for code updates from prior releases to the new DSL. The patterns below apply +equally to "where", "and", and "or" methods from the prior releases. + +### Migrating Single Column and Condition + +In prior releases, a criterion with a single column and condition was written as follows: + +```kotlin +select(foo) { + from(bar) + where(id, isEqualTo(3)) + or(id, isEqualTo(4)) +} +``` + +These criteria should be updated by moving the column and condition into a lambda and using an infix function: + +```kotlin +select(foo) { + from(bar) + where { + id isEqualTo 3 + or { id isEqualTo 4 } + } +} +``` + +### Migrating Compound Column and Condition Criteria + +In prior releases, a criterion with multiple column and conditions grouped together was written like the following: + +```kotlin +select(foo) { + from(bar) + where(id, isEqualTo(3)) { + or(id, isEqualTo(4)) + } +} +``` + +These criteria should be updated by moving the first column and condition into the lambda, using infix functions, +and updating the second criterion as well: + +```kotlin +select(foo) { + from(bar) + where { + id isEqualTo 3 + or { id isEqualTo 4 } + } +} +``` + +### Migrating Criteria Using Filter and Map + +In prior releases, a criterion that used filter and map was written as follows: + +```kotlin +select(foo) { + from(bar) + where(firstName, isLike("fred").map { "%$it%" }) // add SQL wildcards +} +``` + +These criteria should be updated by moving the column and condition into a lambda and using the "invoke" operator +function: + +```kotlin +select(foo) { + from(bar) + where { firstName (isLike("fred").map { "%$it%" }) } // add SQL wildcards +} +``` + +### Migrating Exists Criteria + +In prior releases, a criterion that used an "exists" sub-query looked like this: + +```kotlin +select(foo) { + from(bar) + where( + exists { + select(baz) { + from(bar) + } + } + ) +} +``` + +These criteria should be updated by moving the "exists" phrase into a lambda: + +```kotlin +select(foo) { + from(bar) + where { + exists { + select(baz) { + from(bar) + } + } + } +} +``` + +### Migrating Not Exists Criteria + +In prior releases, a criterion that used a "not exists" sub-query looked like this: + +```kotlin +select(foo) { + from(bar) + where( + notExists { + select(baz) { + from(bar) + } + } + ) +} +``` + +These criteria should be updated by moving the phrase into a lambda, and replacing "notExists" with a combination +of "not" and "exists": + +```kotlin +select(foo) { + from(bar) + where { + not { + exists { + select(baz) { + from(bar) + } + } + } + } +} +``` + +### Migrating Compound Exists Criteria + +In prior releases, a criterion that used a compound "exists" sub-query looked like this: + +```kotlin +select(foo) { + from(bar) + where( + exists { + select(baz) { + from(bar) + } + } + ) { + or(id, isEqualTo(3)) + } +} +``` + +These criteria should be updated by moving the "exists" phrase into the lambda and updating any other criteria: + +```kotlin +select(foo) { + from(bar) + where { + exists { + select(baz) { + from(bar) + } + } + or { id isEqualTo 3 } + } +} +``` diff --git a/src/site/markdown/docs/migratingV1toV2.md b/src/site/markdown/docs/migratingV1toV2.md new file mode 100644 index 000000000..12b6adc37 --- /dev/null +++ b/src/site/markdown/docs/migratingV1toV2.md @@ -0,0 +1,49 @@ +# V1 to V2 Migration Guide + +Version 2 of MyBatis Dynamic SQL introduced many new features. On this page we will provide examples for the more +significant changes - changes that are more substantial than following deprecation messages. + +## Kotlin Join Syntax + +The Java DSL for joins was changed to allow much more flexible joins. Of course, not all capabilities are supported in +all databases, but you should now be able to code most joins specification that are supported by your database. +The changes in the Java DSL are mostly internal and should not impact most users. The `equalTo` methods has been +deprecated in favor of `isEqualTo`, but all other changes should be hidden. + +Like the Java DSL, the V2 Kotlin DSL offers a fully flexible join specification and allows for much more flexible join +specifications. The changes in the Kotlin DSL allow a more natural expressions of a join specification. The main +difference is that the "on" keyword should be moved outside the join specification lambda (it is now an infix function). +Inside the lambda, the conditions should be rewritten to match the syntax of a where clause. + +V1 (Deprecated) Join Specification Example: +```kotlin +val selectStatement = select( + orderMaster.orderId, orderMaster.orderDate, + orderDetail.lineNumber, orderDetail.description, orderDetail.quantity +) { + from(orderMaster, "om") + join(orderDetail, "od") { + on(orderMaster.orderId) equalTo orderDetail.orderId + and(orderMaster.orderId) equalTo constant("1") + } +} +``` + +V2 Join Specification Example: +```kotlin +val selectStatement = select( + orderMaster.orderId, orderMaster.orderDate, + orderDetail.lineNumber, orderDetail.description, orderDetail.quantity +) { + from(orderMaster, "om") + join(orderDetail, "od") on { + orderMaster.orderId isEqualTo orderDetail.orderId + and { orderMaster.orderId isEqualTo constant("1") } + } +} +``` + +Notice that the "on" keyword has been moved outside the lambda, and the conditions are coded with the same syntax used +by WHERE, HAVING, and CASE expressions. + +The prior syntax is deprecated and will be removed in a future release. diff --git a/src/site/markdown/docs/mybatis3.md b/src/site/markdown/docs/mybatis3.md index bed33e2bc..5af522b7b 100644 --- a/src/site/markdown/docs/mybatis3.md +++ b/src/site/markdown/docs/mybatis3.md @@ -1,30 +1,201 @@ # Specialized Support for MyBatis3 -Most of the examples shown on this site are for usage with MyBatis3 - even though the library does support other SQL runtimes like Spring JDBC templates. In addition to the examples shown elsewhere, the library has additional specialized support for MyBatis3 beyond what is shown in the other examples. This support mainly exists to support MyBatis Generator and the code generated by that tool. Even without MyBatis Generator, some of the techniques shown on this page may prove useful. +Most of the examples shown on this site are for usage with MyBatis3 - even though the library does support other SQL +runtimes like Spring JDBC templates. In addition to the examples shown elsewhere, the library has additional specialized +support for MyBatis3 beyond what is shown in the other examples. This support mainly exists to support MyBatis Generator +and the code generated by that tool. Even without MyBatis Generator, the techniques shown on this page may prove useful. + +The goal of this support is to reduce the amount of boilerplate code needed for a typical CRUD mapper. For example, this +support allows you to create a reusable SELECT method where the user only needs to specify a WHERE clause. + +With version 1.1.3, specialized interfaces and utilities were added that can further simplify client code. This support +enables the creation of methods that have similar functionality to methods generated in previous versions of MyBatis +generator like countByExample, deleteByExample, and selectByExample. We no longer use the "by example" terms for these +methods as this library has eliminated the Example class that was generated by prior versions of MyBatis Generator. + +## Common Mapper Support +The library includes several common mappers for MyBatis that can be injected into a MyBatis configuration as-is, or can be +extended. These mappers can be used to eliminate repetitive boilerplate code for several operations - namely count queries, +deletes, inserts, and updates. In addition, there is a common select mapper that can be used to avoid writing custom +result maps for every query. The common select mapper provides a row mapper function that is very similar to Spring +JDBC template. + +### Common Count, Delete, Insert, and Update Mappers +These mappers provide utility functions that execute simple queries. They can be used as-as, or can be extended. They +provide methods as follows: + +| Mapper | Methods(s) | +|--------------------------------------------------------------------------------------------------------|---| +| `org.mybatis.dynamic.sql.util.mybatis3.CommonCountMapper` | `long count(SelectStatementProvider)` | +| `org.mybatis.dynamic.sql.util.mybatis3.CommonDeleteMapper` | `int delete(DeleteStatementProvider)` +| `org.mybatis.dynamic.sql.util.mybatis3.CommonInsertMapper`
(extends `CommonGeneralInsertMapper`) | `int insert(InsertStatementProvider)`
`int insertMultiple(MultiRowInsertStatementProvider)` | +| `org.mybatis.dynamic.sql.util.mybatis3.CommonGeneralInsertMapper` | `int generalInsert(GeneralInsertStatementProvider)`
`int insertSelect(InsertSelectStatementProvider)` | +| `org.mybatis.dynamic.sql.util.mybatis3.CommonUpdateMapper` | `int update(UpdateStatementProvider)` | + +These mappers, as well as the `CommonSelectMapper`, can be used to create a general purpose CRUD mapper as follows: -The goal of this support is to reduce the amount of boilerplate code needed for a typical CRUD mapper. For example, this support allows you to create a reusable SELECT method where the user only needs to specify a WHERE clause. +```java +import org.apache.ibatis.annotations.Mapper; +import org.mybatis.dynamic.sql.util.mybatis3.CommonCountMapper; +import org.mybatis.dynamic.sql.util.mybatis3.CommonDeleteMapper; +import org.mybatis.dynamic.sql.util.mybatis3.CommonInsertMapper; +import org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper; +import org.mybatis.dynamic.sql.util.mybatis3.CommonUpdateMapper; + +@Mapper +public interface FooMapper extends CommonCountMapper, CommonDeleteMapper, CommonInsertMapper, CommonSelectMapper, + CommonUpdateMapper { +} +``` + +This mapper can be extended with default methods as shown below. + +### Common Select Mapper +MyBatis is very good at mapping result sets to objects - this is one of its primary differentiators. MyBatis also requires +that you predefine the mappings for every possibility. This presents a challenge if you want very dynamic column lists +in a query. This library provides a generalized MyBatis mapper that can assist with that problem. + +The general mapper is `org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper`. This mapper can be injected into a +MyBatis configuration as is, or it can be extended by an existing mapper. + +The mapper contains three types of methods: + +1. The `selectOneMappedRow` and `selectManyMappedRows` methods allow you to use select statements with +any number of columns. MyBatis will process the rows and return a Map of values, or a List of Maps for multiple rows. +1. The `selectOne` and `selectMany` methods also allow you to use select statements with any number of columns. These methods +also allow you to specify a function that will transform a Map of row values into a specific object. +1. The other methods are for result sets with a single column. There are functions for many +data types (Integer, Long, String, etc.) There are also functions that return a single value, and Optional value, +or a List of values. + +An example of using the mapped row methods follows: + +```java +package foo.service; +import static org.mybatis.dynamic.sql.SqlBuilder.*; + +import java.util.List; +import java.util.Map; +import org.mybatis.dynamic.sql.render.RenderingStrategies; +import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; +import org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper; + +public class MyService { + public List> generalSearch() { + CommonSelectMapper mapper = getGeneralMapper(); // not shown + + SelectStatementProvider selectStatement = select(id, description) + .from(foo) + .where(description. isLike("%bar%")) + .build() + .render(RenderingStrategies.MYBATIS3); + return mapper.selectManyMappedRows(selectStatement); + } +} +``` + +As you can see, the method returns a List of Maps containing the row values. The Map key will be the column name as +returned from the database (typically in upper case), and the column value as returned from the `ResultSet.getObject()`. +See your JDBC driver's documentation for details about how SQL types are mapped to Java types to determine the data type +for your specific database. + +This method works well, but usually it is better to marshal the result set into actual objects. This can be accomplished +as follows: + +```java +package foo.service; +import static org.mybatis.dynamic.sql.SqlBuilder.*; + +import java.util.List; +import org.mybatis.dynamic.sql.render.RenderingStrategies; +import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; +import org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper; + +public class MyService { + public List generalSearch() { + CommonSelectMapper mapper = getGeneralMapper(); // not shown + + SelectStatementProvider selectStatement = select(id, description) + .from(foo) + .where(description. isLike("%bar%")) + .build() + .render(RenderingStrategies.MYBATIS3); + return mapper.selectMany(selectStatement, m -> { + TableCode tc = new TableCode(); + tc.setId((Integer) m.get("ID")); + tc.setDescription((String) m.get("DESCRIPTION")); + return tc; + }); + } +} +``` + +With this method you can centralize all the database specific operations in a single method. + +If you only have a single column in the result set, the general mapper provides methods to retrieve the value directly. +For example: + +```java +package foo.service; +import static org.mybatis.dynamic.sql.SqlBuilder.*; + +import org.mybatis.dynamic.sql.render.RenderingStrategies; +import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; +import org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper; -With version 1.1.3, specialized interfaces and utilities were added that can further simplify client code. This support enables the creation of methods that have similar functionality to some of the methods generated in previous versions of MyBatis generator like countByExample, deleteByExample, and selectByExample. We no longer use the "by example" terms for these methods as this library has eliminated the Example class that was generated by prior versions of MyBatis Generator. +public class MyService { + public Long getAverageAge() { + CommonSelectMapper mapper = getGeneralMapper(); // not shown + + SelectStatementProvider selectStatement = select(avg(age)) + .from(foo) + .where(description. isLike("%bar%")) + .build() + .render(RenderingStrategies.MYBATIS3); + return mapper.selectOneLong(selectStatement); + } +} +``` ## Count Method Support -The goal of count method support is to enable the creation of methods that execute a count query allowing a user to specify a where clause at runtime, but abstracting away all other details. +The goal of count method support is to enable the creation of methods that execute a count query allowing a user to +specify a where clause at runtime, but abstracting away all other details. -To use this support, we envision creating two methods on a MyBatis mapper interface. The first method is the standard MyBatis Dynamic SQL method that will execute a select: +To use this support, we envision creating several methods on a MyBatis mapper interface. The first method is the +standard MyBatis Dynamic SQL method that will execute a select: ```java @SelectProvider(type=SqlProviderAdapter.class, method="select") long count(SelectStatementProvider selectStatement); ``` -This is a standard method for MyBatis Dynamic SQL that executes a query and returns a `long`. The second method will reuse this method and supply everything needed to build the select statement except the where clause: +This is a standard method for MyBatis Dynamic SQL that executes a query and returns a `long`. The other methods will reuse +this method and supply everything needed to build the select statement except the where clause. In lieu of writing this method, +you could extend `org.mybatis.dynamic.sql.util.mybatis3.CommonCountMapper` instead. There are several variants +of count queries that may be useful: + +1. `count(*)` - counts the number of rows that match a where clause +1. `count(column)` - counts the number of non-null column values that match a where clause +1. `count(distinct column)` - counts the number of unique column values that match a where clause + +Corresponding mapper methods are as follows: ```java -default long count(CountDSLCompleter completer) { +default long count(CountDSLCompleter completer) { // count(*) return MyBatis3Utils.countFrom(this::count, person, completer); } + +default long count(BasicColumn column, CountDSLCompleter completer) { // count(column) + return MyBatis3Utils.count(this::count, column, person, completer); +} + +default long countDistinct(BasicColumn column, CountDSLCompleter completer) { // count(distinct column) + return MyBatis3Utils.countDistinct(this::count, column, person, completer); +} ``` -This method shows the use of `CountDSLCompleter` which is a specialization of a `java.util.Function` that will allow a user to supply a where clause. Clients can use the method as follows: +These methods show the use of `CountDSLCompleter` which is a specialization of a `java.util.Function` that will allow +a user to supply a where clause. Clients can use the method as follows: ```java long rows = mapper.count(c -> @@ -39,16 +210,20 @@ long rows = mapper.count(CountDSLCompleter.allRows()); ## Delete Method Support -The goal of delete method support is to enable the creation of methods that execute a delete statement allowing a user to specify a where clause at runtime, but abstracting away all other details. +The goal of delete method support is to enable the creation of methods that execute a delete statement allowing a user +to specify a where clause at runtime, but abstracting away all other details. -To use this support, we envision creating two methods on a MyBatis mapper interface. The first method is the standard MyBatis Dynamic SQL method that will execute a delete: +To use this support, we envision creating two methods on a MyBatis mapper interface. The first method is the standard +MyBatis Dynamic SQL method that will execute a delete: ```java @DeleteProvider(type=SqlProviderAdapter.class, method="delete") int delete(DeleteStatementProvider deleteStatement); ``` -This is a standard method for MyBatis Dynamic SQL that executes a delete and returns an `int` - the number of rows deleted. The second method will reuse this method and supply everything needed to build the delete statement except the where clause: +This is a standard method for MyBatis Dynamic SQL that executes a delete and returns an `int` - the number of rows deleted. +In lieu of writing this method, you could extend `org.mybatis.dynamic.sql.util.mybatis3.CommonDeleteMapper` instead. +The second method will reuse this method and supply everything needed to build the delete statement except the where clause: ```java default int delete(DeleteDSLCompleter completer) { @@ -56,7 +231,8 @@ default int delete(DeleteDSLCompleter completer) { } ``` -This method shows the use of `DeleteDSLCompleter` which is a specialization of a `java.util.Function` that will allow a user to supply a where clause. Clients can use the method as follows: +This method shows the use of `DeleteDSLCompleter` which is a specialization of a `java.util.Function` that will allow a +user to supply a where clause. Clients can use the method as follows: ```java int rows = mapper.delete(c -> @@ -73,23 +249,33 @@ int rows = mapper.delete(DeleteDSLCompleter.allRows()); The goal of insert method support is to remove some of the boilerplate code from insert methods in a mapper interfaces. -To use this support, we envision creating several methods on a MyBatis mapper interface. The first two methods are the standard MyBatis Dynamic SQL method that will execute an insert: +To use this support, we envision creating several methods on a MyBatis mapper interface. The first methods are the +standard MyBatis methods that will execute an insert: ```java @InsertProvider(type=SqlProviderAdapter.class, method="insert") int insert(InsertStatementProvider insertStatement); +@InsertProvider(type=SqlProviderAdapter.class, method="generalInsert") +int generalInsert(GeneralInsertStatementProvider insertStatement); + @InsertProvider(type=SqlProviderAdapter.class, method="insertMultiple") int insertMultiple(MultiRowInsertStatementProvider insertStatement); ``` -These two methods are standard methods for MyBatis Dynamic SQL. They execute a single row insert and a multiple row insert. +These methods are standard methods for MyBatis Dynamic SQL. They execute a single row insert, a general insert, and a +multiple row insert. In lieu of writing these methods, you could extend +`org.mybatis.dynamic.sql.util.mybatis3.CommonInsertMapper` instead. These methods can be used to implement simplified insert methods: ```java +default int insert(UnaryOperator completer) { + return MyBatis3Utils.insert(this::generalInsert, person, completer); +} + default int insert(PersonRecord record) { - return MyBatis3Utils.insert(this::insert, record, person, c -> + return MyBatis3Utils.insert(this::insert, record, person, c -> c.map(id).toProperty("id") .map(firstName).toProperty("firstName") .map(lastName).toProperty("lastName") @@ -117,13 +303,17 @@ default int insertMultiple(Collection records) { } ``` -In the mapper, only the column mappings need to be specified and no other boilerplate code is needed. +The first insert method is a general insert and can be used to create arbitrary inserts with different combinations of +columns specified. The other methods have the insert statements mapped to a POJO "record" class that holds values for +the insert statement. ## Select Method Support -The goal of select method support is to enable the creation of methods that execute a select statement allowing a user to specify a where clause and/or order by clause at runtime, but abstracting away all other details. +The goal of select method support is to enable the creation of methods that execute a select statement allowing a user +to specify a where clause and/or order by clause at runtime, but abstracting away all other details. -To use this support, we envision creating several methods on a MyBatis mapper interface. The first two methods are the standard MyBatis Dynamic SQL method that will execute a select: +To use this support, we envision creating several methods on a MyBatis mapper interface. The first two methods are the +standard MyBatis Dynamic SQL method that will execute a select: ```java @SelectProvider(type=SqlProviderAdapter.class, method="select") @@ -136,13 +326,14 @@ To use this support, we envision creating several methods on a MyBatis mapper in @Result(column="occupation", property="occupation", jdbcType=JdbcType.VARCHAR) }) List selectMany(SelectStatementProvider selectStatement); - + @SelectProvider(type=SqlProviderAdapter.class, method="select") @ResultMap("PersonResult") Optional selectOne(SelectStatementProvider selectStatement); ``` -These two methods are standard methods for MyBatis Dynamic SQL. They execute a select and return either a list of records, or a single record. +These two methods are standard methods for MyBatis Dynamic SQL. They execute a select and return either a list of +records, or a single record. We also envision creating a static field for a reusable list of columns for a select statement: @@ -159,7 +350,8 @@ default Optional selectOne(SelectDSLCompleter completer) { } ``` -This method shows the use of `SelectDSLCompleter` which is a specialization of a `java.util.Function` that will allow a user to supply a where clause. +This method shows the use of `SelectDSLCompleter` which is a specialization of a `java.util.Function` that will allow a +user to supply a where clause. The general `selectOne` method can be used to implement a `selectByPrimaryKey` method: @@ -171,19 +363,21 @@ default Optional selectByPrimaryKey(Integer id_) { } ``` -The `selectMany` method can be used to implement generalized select methods where a user can specify a where clause and/or an order by clause. Typically we recommend two of these methods - for select, and select distinct: +The `selectMany` method can be used to implement generalized select methods where a user can specify a where clause +and/or an order by clause. Typically, we recommend two of these methods - for select, and select distinct: ```java default List select(SelectDSLCompleter completer) { return MyBatis3Utils.selectList(this::selectMany, selectList, person, completer); } - + default List selectDistinct(SelectDSLCompleter completer) { return MyBatis3Utils.selectDistinct(this::selectMany, selectList, person, completer); } ``` -These methods show the use of `MyBatis3SelectListHelper` which is a specialization of a `java.util.Function` that will allow a user to supply a where clause and/or an order by clause. +These methods show the use of `SelectDSLCompleter` which is a specialization of a `java.util.Function` that will +allow a user to supply a where clause and/or an order by clause. Clients can use the methods as follows: @@ -197,28 +391,33 @@ There are utility methods that will select all rows in a table: ```java List rows = - mapper.selectByExample(SelectDSLCompleter.allRows()); + mapper.select(SelectDSLCompleter.allRows()); ``` The following query will select all rows in a specified order: ```java List rows = - mapper.selectByExample(SelectDSLCompleter.allRowsOrderedBy(lastName, firstName)); + mapper.select(SelectDSLCompleter.allRowsOrderedBy(lastName, firstName)); ``` ## Update Method Support -The goal of update method support is to enable the creation of methods that execute an update statement allowing a user to specify values to set and a where clause at runtime, but abstracting away all other details. +The goal of update method support is to enable the creation of methods that execute an update statement allowing a user +to specify values to set and a where clause at runtime, but abstracting away all other details. -To use this support, we envision creating several methods on a MyBatis mapper interface. The first method is a standard MyBatis Dynamic SQL method that will execute a update: +To use this support, we envision creating several methods on a MyBatis mapper interface. The first method is a standard +MyBatis Dynamic SQL method that will execute a update: ```java @UpdateProvider(type=SqlProviderAdapter.class, method="update") int update(UpdateStatementProvider updateStatement); ``` -This is a standard method for MyBatis Dynamic SQL that executes a query and returns an `int` - the number of rows updated. The second method will reuse this method and supply everything needed to build the update statement except the values and the where clause: +This is a standard method for MyBatis Dynamic SQL that executes a query and returns an `int` - the number of rows updated. +In lieu of writing this method, you could extend `org.mybatis.dynamic.sql.util.mybatis3.CommonUpdateMapper` instead. +The second method will reuse this method and supply everything needed to build the update statement except the values +and the where clause: ```java default int update(UpdateDSLCompleter completer) { @@ -226,7 +425,8 @@ default int update(UpdateDSLCompleter completer) { } ``` -This method shows the use of `UpdateDSLCompleter` which is a specialization of a `java.util.Function` that will allow a user to supply values and a where clause. Clients can use the method as follows: +This method shows the use of `UpdateDSLCompleter` which is a specialization of a `java.util.Function` that will allow a +user to supply values and a where clause. Clients can use the method as follows: ```java int rows = mapper.update(c -> @@ -244,14 +444,14 @@ int rows = mapper.update(c -> It is also possible to write a utility method that will set values. For example: ```java -static UpdateDSL updateSelectiveColumns(PersonRecord record, +static UpdateDSL updateSelectiveColumns(PersonRecord row, UpdateDSL dsl) { - return dsl.set(id).equalToWhenPresent(record::getId) - .set(firstName).equalToWhenPresent(record::getFirstName) - .set(lastName).equalToWhenPresent(record::getLastName) - .set(birthDate).equalToWhenPresent(record::getBirthDate) - .set(employed).equalToWhenPresent(record::getEmployed) - .set(occupation).equalToWhenPresent(record::getOccupation); + return dsl.set(id).equalToWhenPresent(row::getId) + .set(firstName).equalToWhenPresent(row::getFirstName) + .set(lastName).equalToWhenPresent(row::getLastName) + .set(birthDate).equalToWhenPresent(row::getBirthDate) + .set(employed).equalToWhenPresent(row::getEmployed) + .set(occupation).equalToWhenPresent(row::getOccupation); } ``` @@ -262,57 +462,3 @@ rows = mapper.update(h -> updateSelectiveColumns(updateRecord, h) .where(id, isEqualTo(100))); ``` - -# Prior Support -Prior to version 1.1.3, it was also possible to write reusable methods, but they were a bit inconsistent with other helper methods. Mappers of this style are deprecated and the support classes for mappers of this style will be removed in a future version of this library. - -For example, it is possible to write a mapper interface like this: - -```java -import static examples.simple.PersonDynamicSqlSupport.*; - -import java.util.List; - -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Result; -import org.apache.ibatis.annotations.Results; -import org.apache.ibatis.annotations.SelectProvider; -import org.apache.ibatis.type.JdbcType; -import org.mybatis.dynamic.sql.select.MyBatis3SelectModelAdapter; -import org.mybatis.dynamic.sql.select.QueryExpressionDSL; -import org.mybatis.dynamic.sql.select.SelectDSL; -import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; -import org.mybatis.dynamic.sql.util.SqlProviderAdapter; - -@Mapper -public interface LegacyPersonMapper { - - @SelectProvider(type=SqlProviderAdapter.class, method="select") - @Results(id="PersonResult", value= { - @Result(column="A_ID", property="id", jdbcType=JdbcType.INTEGER, id=true), - @Result(column="first_name", property="firstName", jdbcType=JdbcType.VARCHAR), - @Result(column="last_name", property="lastName", jdbcType=JdbcType.VARCHAR), - @Result(column="birth_date", property="birthDate", jdbcType=JdbcType.DATE), - @Result(column="employed", property="employed", jdbcType=JdbcType.VARCHAR, typeHandler=YesNoTypeHandler.class), - @Result(column="occupation", property="occupation", jdbcType=JdbcType.VARCHAR) - }) - List selectMany(SelectStatementProvider selectStatement); - - default QueryExpressionDSL>> selectByExample() { - return SelectDSL.selectWithMapper(this::selectMany, id.as("A_ID"), firstName, lastName, birthDate, employed, occupation) - .from(simpleTable); - } -} -``` - -Notice the `selectByExample` method - it specifies the column list and table name and returns the intermediate builder that can be used to finish the WHERE clause. It also reuses the `selectMany` mapper method. Mapper methods built using this added support all finish with an `execute` method that builds the statement and executes the mapper method. - -The code is used like this: - -```java - List rows = mapper.selectByExample() - .where(id, isEqualTo(1)) - .or(occupation, isNull()) - .build() - .execute(); -``` diff --git a/src/site/markdown/docs/quickStart.md b/src/site/markdown/docs/quickStart.md index bf2e5c413..01e9bdb9c 100644 --- a/src/site/markdown/docs/quickStart.md +++ b/src/site/markdown/docs/quickStart.md @@ -9,25 +9,46 @@ Working with MyBatis Dynamic SQL requires the following steps: For the purposes of this discussion, we will show using the library to perform CRUD operations on this table: ```sql -create table SimpleTable ( - id int not null, - first_name varchar(30) not null, - last_name varchar(30) not null, - birth_date date not null, - employed varchar(3) not null, - occupation varchar(30) null, - primary key(id) +create table Person ( + id int not null, + first_name varchar(30) not null, + last_name varchar(30) not null, + birth_date date not null, + employed varchar(3) not null, + occupation varchar(30) null, + address_id int not null, + primary key(id) ); ``` +We will also create a simple Java class to represent a row in the table: + +```java +package examples.simple; + +import java.util.Date; + +public class PersonRecord { + private Integer id; + private String firstName; + private LastName lastName; + private Date birthDate; + private Boolean employed; + private String occupation; + private Integer addressId; + + // getters and setters omitted +} +``` + ## Defining Tables and Columns -The class `org.mybatis.dynamic.sql.SqlTable` is used to define a table. A table definition includes +The class `org.mybatis.dynamic.sql.AlisableSqlTable` is used to define a table. A table definition includes the actual name of the table (including schema or catalog if appropriate). A table alias can be applied in a -select statement if desired. Your table should be defined by extending the `SqlTable` class. +select statement if desired. Your table should be defined by extending the `AlisableSqlTable` class. The class `org.mybatis.dynamic.sql.SqlColumn` is used to define columns for use in the library. -SqlColumns should be created using the builder methods in SqlTable. +SqlColumns should be created using the builder methods in `SqlTable`. A column definition includes: 1. The Java type @@ -36,8 +57,8 @@ A column definition includes: 4. (optional) The name of a type handler to use in MyBatis if the default type handler is not desired We suggest the following usage pattern to give maximum flexibility. This pattern will allow you to use your -table and columns in a "qualified" or "un-qualified" manner that looks like natural SQL. For example, in the -following a column could be referred to as `firstName` or `simpleTable.firstName`. +table and column names in a "qualified" or "un-qualified" manner that looks like natural SQL. For example, in the +following a column could be referred to as `firstName` or `person.firstName`. ```java package examples.simple; @@ -46,36 +67,42 @@ import java.sql.JDBCType; import java.util.Date; import org.mybatis.dynamic.sql.SqlColumn; -import org.mybatis.dynamic.sql.SqlTable; - -public final class SimpleTableDynamicSqlSupport { - public static final SimpleTable simpleTable = new SimpleTable(); - public static final SqlColumn id = simpleTable.id; - public static final SqlColumn firstName = simpleTable.firstName; - public static final SqlColumn lastName = simpleTable.lastName; - public static final SqlColumn birthDate = simpleTable.birthDate; - public static final SqlColumn employed = simpleTable.employed; - public static final SqlColumn occupation = simpleTable.occupation; - - public static final class SimpleTable extends SqlTable { +import org.mybatis.dynamic.sql.AliasableSqlTable; + +public final class PersonDynamicSqlSupport { + public static final Person person = new Person(); + public static final SqlColumn id = person.id; + public static final SqlColumn firstName = person.firstName; + public static final SqlColumn lastName = person.lastName; + public static final SqlColumn birthDate = person.birthDate; + public static final SqlColumn employed = person.employed; + public static final SqlColumn occupation = person.occupation; + public static final SqlColumn addressId = person.addressId; + + public static final class Person extends AliasableSqlTable { public final SqlColumn id = column("id", JDBCType.INTEGER); public final SqlColumn firstName = column("first_name", JDBCType.VARCHAR); - public final SqlColumn lastName = column("last_name", JDBCType.VARCHAR); + public final SqlColumn lastName = column("last_name", JDBCType.VARCHAR, "examples.simple.LastNameTypeHandler"); public final SqlColumn birthDate = column("birth_date", JDBCType.DATE); public final SqlColumn employed = column("employed", JDBCType.VARCHAR, "examples.simple.YesNoTypeHandler"); public final SqlColumn occupation = column("occupation", JDBCType.VARCHAR); + public final SqlColumn addressId = column("address_id", JDBCType.INTEGER); - public SimpleTable() { - super("SimpleTable"); + public Person() { + super("Person", Person::new); } } } ``` ## Creating MyBatis3 Mappers -The library will create classes that will be used as input to a MyBatis mapper. These classes include the generated SQL, as well as a parameter set that will match the generated SQL. Both are required by MyBatis. It is intended that these objects be the one and only parameter to a MyBatis mapper method. +The library will create classes that will be used as input to a MyBatis mapper. These classes include the generated +SQL, as well as a parameter set that will match the generated SQL. Both are required by MyBatis. It is intended that +these objects be the one and only parameter to a MyBatis mapper method. -The library can be used with both XML and annotated mappers, but we recommend using MyBatis' annotated mapper support in all cases. The only case where XML is required is when you code a JOIN statement - in that case you will need to define your result map in XML due to limitations of the MyBatis annotations in supporting joins. +The library can be used with both XML and annotated mappers, but we recommend using MyBatis' annotated mapper support in +all cases. The only case where XML is required is when you code a JOIN statement - in that case you will need to define +your result map in XML due to limitations of the MyBatis annotations in supporting joins. For example, a mapper might look like this: @@ -83,74 +110,89 @@ For example, a mapper might look like this: package examples.simple; import java.util.List; +import java.util.Optional; -import org.apache.ibatis.annotations.DeleteProvider; -import org.apache.ibatis.annotations.InsertProvider; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Result; import org.apache.ibatis.annotations.ResultMap; import org.apache.ibatis.annotations.Results; import org.apache.ibatis.annotations.SelectProvider; -import org.apache.ibatis.annotations.UpdateProvider; import org.apache.ibatis.type.JdbcType; -import org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider; -import org.mybatis.dynamic.sql.insert.render.InsertStatementProvider; import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; -import org.mybatis.dynamic.sql.update.render.UpdateStatementProvider; import org.mybatis.dynamic.sql.util.SqlProviderAdapter; +import org.mybatis.dynamic.sql.util.mybatis3.CommonCountMapper; +import org.mybatis.dynamic.sql.util.mybatis3.CommonDeleteMapper; +import org.mybatis.dynamic.sql.util.mybatis3.CommonInsertMapper; +import org.mybatis.dynamic.sql.util.mybatis3.CommonUpdateMapper; @Mapper -public interface SimpleTableAnnotatedMapper { - - @InsertProvider(type=SqlProviderAdapter.class, method="insert") - int insert(InsertStatementProvider insertStatement); - - @UpdateProvider(type=SqlProviderAdapter.class, method="update") - int update(UpdateStatementProvider updateStatement); - - @SelectProvider(type=SqlProviderAdapter.class, method="select") - @Results(id="SimpleTableResult", value= { - @Result(column="A_ID", property="id", jdbcType=JdbcType.INTEGER, id=true), - @Result(column="first_name", property="firstName", jdbcType=JdbcType.VARCHAR), - @Result(column="last_name", property="lastName", jdbcType=JdbcType.VARCHAR), - @Result(column="birth_date", property="birthDate", jdbcType=JdbcType.DATE), - @Result(column="employed", property="employed", jdbcType=JdbcType.VARCHAR, typeHandler=YesNoTypeHandler.class), - @Result(column="occupation", property="occupation", jdbcType=JdbcType.VARCHAR) +public interface PersonMapper extends CommonCountMapper, CommonDeleteMapper, CommonInsertMapper, CommonUpdateMapper { + + @SelectProvider(type = SqlProviderAdapter.class, method = "select") + @Results(id = "PersonResult", value = { + @Result(column = "A_ID", property = "id", jdbcType = JdbcType.INTEGER, id = true), + @Result(column = "first_name", property = "firstName", jdbcType = JdbcType.VARCHAR), + @Result(column = "last_name", property = "lastName", jdbcType = JdbcType.VARCHAR, typeHandler = LastNameTypeHandler.class), + @Result(column = "birth_date", property = "birthDate", jdbcType = JdbcType.DATE), + @Result(column = "employed", property = "employed", jdbcType = JdbcType.VARCHAR, typeHandler = YesNoTypeHandler.class), + @Result(column = "occupation", property = "occupation", jdbcType = JdbcType.VARCHAR), + @Result(column = "address_id", property = "addressId", jdbcType = JdbcType.INTEGER) }) - List selectMany(SelectStatementProvider selectStatement); - - @SelectProvider(type=SqlProviderAdapter.class, method="select") - @ResultMap("SimpleTableResult") - SimpleTableRecord selectOne(SelectStatementProvider selectStatement); + List selectMany(SelectStatementProvider selectStatement); - @DeleteProvider(type=SqlProviderAdapter.class, method="delete") - int delete(DeleteStatementProvider deleteStatement); - - @SelectProvider(type=SqlProviderAdapter.class, method="select") - long count(SelectStatementProvider selectStatement); + @SelectProvider(type = SqlProviderAdapter.class, method = "select") + @ResultMap("PersonResult") + Optional selectOne(SelectStatementProvider selectStatement); } ``` +This mapper implements full CRUD functionality for the table. The base interfaces `CommonCountMapper`, +`CommonDeleteMapper`, etc. provide insert, update, delete, and count capabilities. Only the select methods must be +written because of the custom result map. + +Note that the `CommonInsertMapper` interface will not properly return the generated key if one is produced by the insert. +If you need generated key support, see the documentation page for INSERT statements for details on how to implement +such support. + ## Executing SQL with MyBatis3 -In a DAO or service class, you can use the generated statement as input to your mapper methods. Here's -an example from `examples.simple.SimpleTableAnnotatedMapperTest`: +In a service class, you can use the generated statement as input to your mapper methods. Here are some +examples from `examples.simple.PersonMapperTest`: ```java - @Test - public void testSelectByExample() { - try (SqlSession session = sqlSessionFactory.openSession()) { - SimpleTableAnnotatedMapper mapper = session.getMapper(SimpleTableAnnotatedMapper.class); - - SelectStatementProvider selectStatement = select(id.as("A_ID"), firstName, lastName, birthDate, employed, occupation) - .from(simpleTable) - .where(id, isEqualTo(1)) - .or(occupation, isNull()) - .build() - .render(RenderingStrategies.MYBATIS3); - - List rows = mapper.selectMany(selectStatement); - - assertThat(rows.size()).isEqualTo(3); - } +@Test +void testGeneralSelect() { + try (SqlSession session = sqlSessionFactory.openSession()) { + PersonMapper mapper = session.getMapper(PersonMapper.class); + + SelectStatementProvider selectStatement = select(id.as("A_ID"), firstName, lastName, birthDate, employed, + occupation, addressId) + .from(person) + .where(id, isEqualTo(1)) + .or(occupation, isNull()) + .build() + .render(RenderingStrategies.MYBATIS3); + + List rows = mapper.selectMany(selectStatement); + assertThat(rows).hasSize(3); } +} + +@Test +void testGeneralDelete() { + try (SqlSession session = sqlSessionFactory.openSession()) { + PersonMapper mapper = session.getMapper(PersonMapper.class); + + DeleteStatementProvider deleteStatement = deleteFrom(person) + .where(occupation, isNull()) + .build() + .render(RenderingStrategies.MYBATIS3); + + int rows = mapper.delete(deleteStatement); + assertThat(rows).isEqualTo(2); + } +} ``` + +If you use MyBatis Generator, the generator will create several additional utility methods in a mapper like this that +will improve its usefulness. You can see a full example of the type of code created by MyBatis generator by looking +at the full example at https://github.com/mybatis/mybatis-dynamic-sql/tree/master/src/test/java/examples/simple diff --git a/src/site/markdown/docs/select.md b/src/site/markdown/docs/select.md index 3696281c5..f73ef17dd 100644 --- a/src/site/markdown/docs/select.md +++ b/src/site/markdown/docs/select.md @@ -6,61 +6,65 @@ select statements, but purposely does not cover every possibility. In general, the following are supported: 1. The typical parts of a select statement including SELECT, DISTINCT, FROM, JOIN, WHERE, GROUP BY, UNION, - UNION ALL, ORDER BY + UNION ALL, ORDER BY, HAVING 2. Tables can be aliased per select statement 3. Columns can be aliased per select statement 4. Some support for aggregates (avg, min, max, sum) -5. Equijoins of type INNER, LEFT OUTER, RIGHT OUTER, FULL OUTER -6. Subqueries in where clauses. For example, `where foo in (select foo from foos where id < 36)` +5. Joins of type INNER, LEFT OUTER, RIGHT OUTER, FULL OUTER +6. Subqueries in where clauses. For example, `where foo in (select foo from foos where id < 36)` +7. Select from another select. For example `select count(*) from (select foo from foos where id < 36)` +8. Multi-Selects. For example `(select * from foo order by id limit 3) union (select * from foo order by id desc limit 3)` At this time, the library does not support the following: 1. WITH expressions -2. HAVING expressions -3. Select from another select. For example `select count(*) from (select foo from foos where id < 36)` -4. INTERSECT, EXCEPT, etc. +2. INTERSECT, EXCEPT, etc. -The user guide page for WHERE Clauses shows examples of many different types of SELECT statements with different complexities of the WHERE clause including support for sub-queries. We will just show a single example here, including an ORDER BY clause: +The user guide page for WHERE Clauses shows examples of many types of SELECT statements with different complexities of +the WHERE clause including support for sub-queries. We will just show a single example here, including an ORDER BY +clause: ```java - SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight) - .from(animalData) - .where(id, isIn(1, 5, 7)) - .and(bodyWeight, isBetween(1.0).and(3.0)) - .orderBy(id.descending(), bodyWeight) - .build() - .render(RenderingStrategies.MYBATIS3); - - List animals = mapper.selectMany(selectStatement); +SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight) + .from(animalData) + .where(id, isIn(1, 5, 7)) + .and(bodyWeight, isBetween(1.0).and(3.0)) + .orderBy(id.descending(), bodyWeight) + .build() + .render(RenderingStrategies.MYBATIS3); + +List animals = mapper.selectMany(selectStatement); ``` The WHERE and ORDER BY clauses are optional. ## Joins -The library supports the generation of equijoin statements - joins defined by column matching. For example: +The library supports the generation of join statements. For example: ```java - SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity) - .from(orderMaster, "om") - .join(orderDetail, "od").on(orderMaster.orderId, equalTo(orderDetail.orderId)) - .build() - .render(RenderingStrategies.MYBATIS3); +SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity) + .from(orderMaster, "om") + .join(orderDetail, "od").on(orderMaster.orderId, isEqualTo(orderDetail.orderId)) + .build() + .render(RenderingStrategies.MYBATIS3); ``` -Notice that you can give an alias to a table if desired. If you don't specify an alias, the full table name will be used in the generated SQL. +Notice that you can give an alias to a table if desired. If you don't specify an alias, the full table name will be +used in the generated SQL. Multiple tables can be joined in a single statement. For example: ```java - SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderLine.lineNumber, itemMaster.description, orderLine.quantity) - .from(orderMaster, "om") - .join(orderLine, "ol").on(orderMaster.orderId, equalTo(orderLine.orderId)) - .join(itemMaster, "im").on(orderLine.itemId, equalTo(itemMaster.itemId)) - .where(orderMaster.orderId, isEqualTo(2)) - .build() - .render(RenderingStrategies.MYBATIS3); +SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderLine.lineNumber, itemMaster.description, orderLine.quantity) + .from(orderMaster, "om") + .join(orderLine, "ol").on(orderMaster.orderId, isEqualTo(orderLine.orderId)) + .join(itemMaster, "im").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .where(orderMaster.orderId, isEqualTo(2)) + .build() + .render(RenderingStrategies.MYBATIS3); ``` -Join queries will likely require you to define a MyBatis result mapping in XML. This is the only instance where XML is required. This is due to the limitations of the MyBatis annotations when mapping collections. +Join queries will likely require you to define a MyBatis result mapping in XML. This is the only instance where XML is +required. This is due to the limitations of the MyBatis annotations when mapping collections. The library supports four join types: @@ -73,23 +77,49 @@ The library supports four join types: The library supports the generation of UNION and UNION ALL queries. For example: ```java - SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight) - .from(animalData) - .union() - .selectDistinct(id, animalName, bodyWeight, brainWeight) - .from(animalData) - .orderBy(id) - .build() - .render(RenderingStrategies.MYBATIS3); +SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight) + .from(animalData) + .union() + .selectDistinct(id, animalName, bodyWeight, brainWeight) + .from(animalData) + .orderBy(id) + .build() + .render(RenderingStrategies.MYBATIS3); ``` Any number of SELECT statements can be added to a UNION query. Only one ORDER BY phrase is allowed. -## Annotated Mapper for Select Statements +With this type of union query, the "order by" and paging clauses are applied to the query as a whole. If +you need to apply "order by" or paging clauses to the nested queries, use a multi-select query as shown +below. + +## Multi-Select Queries + +Multi-select queries are a special case of union select statements. The difference is that "order by" and +paging clauses can be applied to the merged queries. For example: + +```java +SelectStatementProvider selectStatement = multiSelect( + select(id, animalName, bodyWeight, brainWeight) + .from(animalData) + .orderBy(id) + .limit(2) + ).union( + selectDistinct(id, animalName, bodyWeight, brainWeight) + .from(animalData) + .orderBy(id.descending()) + .limit(3) + ) + .build() + .render(RenderingStrategies.MYBATIS3); +``` + +## MyBatis Mapper for Select Statements The SelectStatementProvider object can be used as a parameter to a MyBatis mapper method directly. If you -are using an annotated mapper, the select method should look like this (note that we recommend coding a "selectMany" and a "selectOne" method with a shared result mapping): - +are using an annotated mapper, the select method should look like this (note that we recommend coding a "selectMany" +and a "selectOne" method with a shared result mapping): + ```java import org.apache.ibatis.annotations.Result; import org.apache.ibatis.annotations.ResultMap; @@ -117,7 +147,9 @@ import org.mybatis.dynamic.sql.util.SqlProviderAdapter; ## XML Mapper for Join Statements -If you are coding a join, it is likely you will need to code an XML mapper to define the result map. This is due to a MyBatis limitation - the annotations cannot define a collection mapping. If you have to do this, the Java code looks like this: +If you are coding a join, it is likely you will need to code an XML mapper to define the result map. This is due to a +MyBatis limitation - the annotations cannot define a collection mapping. If you have to do this, the Java code looks +like this: ```java @SelectProvider(type=SqlProviderAdapter.class, method="select") @@ -145,10 +177,11 @@ And the corresponding XML looks like this: Notice that the resultMap is the only element in the XML mapper. This is our recommended practice. ## XML Mapper for Select Statements -We do not recommend using an XML mapper for select statements, but if you want to do so the SelectStatementProvider object can be used as a parameter to a MyBatis mapper method directly. +We do not recommend using an XML mapper for select statements, but if you want to do so the SelectStatementProvider +object can be used as a parameter to a MyBatis mapper method directly. If you are using an XML mapper, the select method should look like this in the Java interface: - + ```java import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; @@ -169,31 +202,43 @@ The XML element should look like this: ``` ## Notes on Order By Order by phrases can be difficult to calculate when there are aliased columns, aliased tables, unions, and joins. -This library has taken a simple approach - the library will either write the column alias or the column -name into the order by phrase. For the order by phrase, the table alias (if there is one) will be ignored. +This library has taken a relatively simple approach: + +1. When specifying an SqlColumn in an ORDER BY phrase the library will either write the column alias or the column + name into the ORDER BY phrase. For the ORDER BY phrase, the table alias (if there is one) will be ignored. Use this + pattern when the ORDER BY column is a member of the select list. For example `orderBy(foo)`. If the column has an + alias, then it is easiest to use the "arbitrary string" method with the column alias as shown below. +2. It is also possible to explicitly specify a table alias for a column in an ORDER BY phrase. Use this pattern when + there is a join, and the ORDER BY column is in two or more tables, and the ORDER BY column is not in the select + list. For example `orderBy(sortColumn("t1", foo))`. +3. If none of the above use cases meet your needs, then you can specify an arbitrary String to write into the rendered + ORDER BY phrase (see below for an example). In our testing, this caused an issue in only one case. When there is an outer join and the select list contains both the left and right join column. In that case, the workaround is to supply a column alias for both columns. -When using a column function (lower, upper, etc.), then is is customary to give the calculated column an alias so you will have a predictable result set. In cases like this there will not be a column to use for an alias. The library supports arbitrary values in an ORDER BY expression like this: +When using a column function (lower, upper, etc.), then it is customary to give the calculated column an alias so you +will have a predictable result set. In cases like this there will not be a column to use for an alias. The library +supports arbitrary values in an ORDER BY expression like this: ```java - SelectStatementProvider selectStatement = select(substring(gender, 1, 1).as("ShortGender"), avg(age).as("AverageAge")) - .from(person, "a") - .groupBy(substring(gender, 1, 1)) - .orderBy(sortColumn("ShortGender").descending()) - .build() - .render(RenderingStrategies.MYBATIS3); +SelectStatementProvider selectStatement = select(substring(gender, 1, 1).as("ShortGender"), avg(age).as("AverageAge")) + .from(person, "a") + .groupBy(substring(gender, 1, 1)) + .orderBy(sortColumn("ShortGender").descending()) + .build() + .render(RenderingStrategies.MYBATIS3); ``` -In this example the `substring` function is used in both the select list and the GROUP BY expression. In the ORDER BY expression, we use the `sortColumn` function to duplicate the alias given to the column in the select list. +In this example the `substring` function is used in both the select list and the GROUP BY expression. In the ORDER BY +expression, we use the `sortColumn` function to duplicate the alias given to the column in the select list. ## Limit and Offset Support Since version 1.1.1 the select statement supports limit and offset for paging (or slicing) queries. You can specify: @@ -202,18 +247,22 @@ Since version 1.1.1 the select statement supports limit and offset for paging (o - Offset only - Both limit and offset -It is important to note that the select renderer writes limit and offset clauses into the generated select statement as is. The library does not attempt to normalize those values for databases that don't support limit and offset directly. Therefore, it is very important for users to understand whether or not the target database supports limit and offset. If the target database does not support limit and offset, then it is likely that using this support will create SQL that has runtime errors. +It is important to note that the select renderer writes limit and offset clauses into the generated select statement as +is. The library does not attempt to normalize those values for databases that don't support limit and offset directly. +Therefore, it is very important for users to understand whether the target database supports limit and offset. +If the target database does not support limit and offset, then it is likely that using this support will create SQL +that has runtime errors. An example follows: ```java - SelectStatementProvider selectStatement = select(animalData.allColumns()) - .from(animalData) - .orderBy(id) - .limit(3) - .offset(22) - .build() - .render(RenderingStrategies.MYBATIS3); +SelectStatementProvider selectStatement = select(animalData.allColumns()) + .from(animalData) + .orderBy(id) + .limit(3) + .offset(22) + .build() + .render(RenderingStrategies.MYBATIS3); ``` ## Fetch First Support @@ -228,11 +277,11 @@ Fetch first is an SQL standard and is supported by most databases. An example follows: ```java - SelectStatementProvider selectStatement = select(animalData.allColumns()) - .from(animalData) - .orderBy(id) - .offset(22) - .fetchFirst(3).rowsOnly() - .build() - .render(RenderingStrategies.MYBATIS3); +SelectStatementProvider selectStatement = select(animalData.allColumns()) + .from(animalData) + .orderBy(id) + .offset(22) + .fetchFirst(3).rowsOnly() + .build() + .render(RenderingStrategies.MYBATIS3); ``` diff --git a/src/site/markdown/docs/spring.md b/src/site/markdown/docs/spring.md index 45e3a98c8..d247365cd 100644 --- a/src/site/markdown/docs/spring.md +++ b/src/site/markdown/docs/spring.md @@ -12,18 +12,47 @@ The SQL statement objects are created in exactly the same way as for MyBatis - o .render(RenderingStrategies.SPRING_NAMED_PARAMETER); ``` -## Limitations +The generated SQL statement providers are compatible with Spring's `NamedParameterJdbcTemplate` in all cases. The only challenge comes with presenting statement parameters to Spring in the correct manner. To make this easier, the library provides a utility class `org.mybatis.dynamic.sql.util.spring.NamedParameterJdbcTemplateExtensions` that executes statements properly in all cases and hides the complexity of rendering statements and formatting parameters. All the examples below will show usage both with and without the utility class. -MyBatis3 is a higher level abstraction over JDBC than Spring JDBC templates. While most functions in the library will work with Spring, there are some functions that do not work: +## Type Converters for Spring -1. Spring JDBC templates do not have anything equivalent to a type handler in MyBatis3. Therefore it is best to use data types that can be automatically understood by Spring -1. The multiple row insert statement *will not* render properly for Spring. However, batch inserts *will* render properly for Spring +Spring JDBC templates do not have the equivalent of a type handler in MyBatis3. This is generally not a problem in processing results because you can build type conversions into your row handler. If you were manually creating the parameter map that is used as input to a Spring template you could perform a type conversion there too. But when you use MyBatis Dynamic SQL, the parameters are generated by the library, so you do not have the opportunity to perform type conversions directly. + +To address this issue, the library provides a parameter type converter that can be used to perform a type conversion before parameters are placed in a parameter map. + +For example, suppose we want to use a `Boolean` in Java to represent the value of a flag, but in the database the corresponding field is a `CHAR` field that expects values "true" or "false". This can be accomplished by using a `ParameterTypeConverter`. First create the converter as follows: + +```java +public class TrueFalseParameterConverter implements ParameterTypeConverter { + @Override + public String convert(Boolean source) { + return source == null ? null : source ? "true" : "false"; + } +} +``` + +The type converter is compatible with Spring's existing Converter interface. Associate the type converter with a SqlColumn as follows: + +```java +... + public final SqlColumn employed = column("employed", JDBCType.VARCHAR) + .withParameterTypeConverter(new TrueFalseParameterConverter()); +... +``` + +MyBatis Dynamic SQL will now call the converter function before corresponding parameters are placed into the generated parameter map. The converter will be called in the following cases: + +1. With a general insert statement when using the `set(...).toValue(...)` or `set(...).toValueWhenPresent(...)` mappings +1. With an update statement when using the `set(...).equalTo(...)` or `set(...).equalToWhenPresent(...)` mappings +1. With where clauses in any statement type that contain conditions referencing the field ## Executing Select Statements -The Spring Named Parameter JDBC template expects an SQL statement with parameter markers in the Spring format, and a set of matched parameters. MyBatis Dynamic SQL will generate both. The parameters returned from the generated SQL statement can be wrapped in a Spring `MapSqlParameterSource`. Spring also expects you to provide a row mapper for creating the returned objects. The following code shows a complete example: +The Spring Named Parameter JDBC template expects an SQL statement with parameter markers in the Spring format, and a set of matched parameters. MyBatis Dynamic SQL will generate both. The parameters returned from the generated SQL statement can be wrapped in a Spring `MapSqlParameterSource`. Spring also expects you to provide a row mapper for creating the returned objects. + +The following code shows a complete example without the utility class: ```java - NamedParameterJdbcTemplate template = getTemplate(); + NamedParameterJdbcTemplate template = getTemplate(); // not shown SelectStatementProvider selectStatement = select(id, firstName, lastName, fullName) .from(generatedAlways) @@ -31,33 +60,133 @@ The Spring Named Parameter JDBC template expects an SQL statement with parameter .orderBy(id.descending()) .build() .render(RenderingStrategies.SPRING_NAMED_PARAMETER); - + SqlParameterSource namedParameters = new MapSqlParameterSource(selectStatement.getParameters()); List records = template.query(selectStatement.getSelectStatement(), namedParameters, - new RowMapper(){ - @Override - public GeneratedAlwaysRecord mapRow(ResultSet rs, int rowNum) throws SQLException { - GeneratedAlwaysRecord record = new GeneratedAlwaysRecord(); - record.setId(rs.getInt(1)); - record.setFirstName(rs.getString(2)); - record.setLastName(rs.getString(3)); - record.setFullName(rs.getString(4)); - return record; - } - }); + (rs, rowNum) -> { + GeneratedAlwaysRecord record = new GeneratedAlwaysRecord(); + record.setId(rs.getInt(1)); + record.setFirstName(rs.getString(2)); + record.setLastName(rs.getString(3)); + record.setFullName(rs.getString(4)); + return record; + }); +``` + +The following code shows a complete example with the utility class: + +```java + NamedParameterJdbcTemplate template = getTemplate(); // not shown + NamedParameterJdbcTemplateExtensions extensions = new NamedParameterJdbcTemplateExtensions(template); + + Buildable selectStatement = select(id, firstName, lastName, fullName) + .from(generatedAlways) + .where(id, isGreaterThan(3)) + .orderBy(id.descending()); + + List records = extensions.selectList(selectStatement, + (rs, rowNum) -> { + GeneratedAlwaysRecord record = new GeneratedAlwaysRecord(); + record.setId(rs.getInt(1)); + record.setFirstName(rs.getString(2)); + record.setLastName(rs.getString(3)); + record.setFullName(rs.getString(4)); + return record; + }); +``` + +The utility class also includes a `selectOne` method that returns an `Optional`. An example is shown below: + +```java + NamedParameterJdbcTemplate template = getTemplate(); // not shown + NamedParameterJdbcTemplateExtensions extensions = new NamedParameterJdbcTemplateExtensions(template); + + Buildable selectStatement = select(id, firstName, lastName, fullName) + .from(generatedAlways) + .where(id, isEqualTo(3)); + + Optional record = extensions.selectOne(selectStatement, + (rs, rowNum) -> { + GeneratedAlwaysRecord record = new GeneratedAlwaysRecord(); + record.setId(rs.getInt(1)); + record.setFirstName(rs.getString(2)); + record.setLastName(rs.getString(3)); + record.setFullName(rs.getString(4)); + return record; + }); ``` ## Executing Insert Statements -Insert statements are a bit different - MyBatis Dynamic SQL generates a properly formatted SQL string for Spring, but instead of a map of parameters, the parameter mappings are created for the inserted record itself. So the parameters for the Spring template are created by a `BeanPropertySqlParameterSource`. Generated keys in Spring are supported with a `GeneratedKeyHolder`. The following is a complete example: + +The library generates several types of insert statements. See the [Insert Statements](insert.html) page for details. + +Spring supports retrieval of generated keys for many types of inserts. This library has support for generated key retrieval where it is supported by Spring. + +### Executing General Insert Statements +General insert statements do not require a POJO object matching a table row. Following is a complete example: + +```java + NamedParameterJdbcTemplate template = getTemplate(); // not shown + + GeneralInsertStatementProvider insertStatement = insertInto(generatedAlways) + .set(id).toValue(100) + .set(firstName).toValue("Bob") + .set(lastName).toValue("Jones") + .build() + .render(RenderingStrategies.SPRING_NAMED_PARAMETER); + + int rows = template.update(insertStatement.getInsertStatement(), insertStatement.getParameters()); +``` + +If you want to retrieve generated keys for a general insert statement the steps are similar except that you must wrap the parameters in a `MapSqlParameterSource` object and use a `GeneratedKeyHolder`. Following is a complete example of this usage: ```java - NamedParameterJdbcTemplate template = getTemplate(); + NamedParameterJdbcTemplate template = getTemplate(); // not shown + + GeneralInsertStatementProvider insertStatement = insertInto(generatedAlways) + .set(id).toValue(100) + .set(firstName).toValue("Bob") + .set(lastName).toValue("Jones") + .build() + .render(RenderingStrategies.SPRING_NAMED_PARAMETER); + + MapSqlParameterSource parameterSource = new MapSqlParameterSource(insertStatement.getParameters()); + KeyHolder keyHolder = new GeneratedKeyHolder(); + + int rows = template.update(insertStatement.getInsertStatement(), parameterSource, keyHolder); + String generatedKey = (String) keyHolder.getKeys().get("FULL_NAME"); +``` + +This can be simplified by using the utility class as follows: + +```java + NamedParameterJdbcTemplate template = getTemplate(); // not shown + NamedParameterJdbcTemplateExtensions extensions = new NamedParameterJdbcTemplateExtensions(template); + + Buildable insertStatement = insertInto(generatedAlways) + .set(id).toValue(100) + .set(firstName).toValue("Bob") + .set(lastName).toValue("Jones"); + + // no generated key retrieval + int rows = extensions.generalInsert(insertStatement); + + // retrieve generated keys + KeyHolder keyHolder = new GeneratedKeyHolder(); + int rows = extensions.generalInsert(insertStatement, keyHolder); +``` + +### Executing Single Record Insert Statements +Insert record statements are a bit different - MyBatis Dynamic SQL generates a properly formatted SQL string for Spring, but instead of a map of parameters, the parameter mappings are created for the inserted record itself. So the parameters for the Spring template are created by a `BeanPropertySqlParameterSource`. Generated keys in Spring are supported with a `GeneratedKeyHolder`. The following is a complete example: + +```java + NamedParameterJdbcTemplate template = getTemplate(); // not shown GeneratedAlwaysRecord record = new GeneratedAlwaysRecord(); record.setId(100); record.setFirstName("Bob"); record.setLastName("Jones"); - + InsertStatementProvider insertStatement = insert(record) .into(generatedAlways) .map(id).toProperty("id") @@ -65,19 +194,80 @@ Insert statements are a bit different - MyBatis Dynamic SQL generates a properly .map(lastName).toProperty("lastName") .build() .render(RenderingStrategies.SPRING_NAMED_PARAMETER); - + SqlParameterSource parameterSource = new BeanPropertySqlParameterSource(insertStatement.getRecord()); KeyHolder keyHolder = new GeneratedKeyHolder(); - + int rows = template.update(insertStatement.getInsertStatement(), parameterSource, keyHolder); String generatedKey = (String) keyHolder.getKeys().get("FULL_NAME"); ``` -## Executing Batch Inserts -Batch insert support in Spring is a bit different than batch support in MyBatis3 and Spring does not support returning generated keys from a batch insert. The following is a complete example of a batch insert (note the use of `SqlParameterSourceUtils` to create an array of parameter sources from an array of input records): +This can be simplified by using the utility class as follows: + +```java + NamedParameterJdbcTemplate template = getTemplate(); // not shown + NamedParameterJdbcTemplateExtensions extensions = new NamedParameterJdbcTemplateExtensions(template); + + GeneratedAlwaysRecord record = new GeneratedAlwaysRecord(); + record.setId(100); + record.setFirstName("Bob"); + record.setLastName("Jones"); + + Buildable> insertStatement = insert(record) + .into(generatedAlways) + .map(id).toProperty("id") + .map(firstName).toProperty("firstName") + .map(lastName).toProperty("lastName"); + + // no generated key retrieval + int rows = extensions.insert(insertStatement); + + // retrieve generated keys + KeyHolder keyHolder = new GeneratedKeyHolder(); + int rows = extensions.insert(insertStatement, keyHolder); +``` + +### Multi-Row Inserts +A multi-row insert is a single insert statement with multiple VALUES clauses. This can be a convenient way in insert a small number of records into a table with a single statement. Note however that a multi-row insert is not suitable for large bulk inserts as it is possible to exceed the limit of prepared statement parameters with a large number of records. For that use case, use a batch insert (see below). + +With multi-row insert statements MyBatis Dynamic SQL generates a properly formatted SQL string for Spring. Instead of a map of parameters, the multiple records are stored in the generated provider object and the parameter mappings are created for the generated provider itself. The parameters for the Spring template are created by a `BeanPropertySqlParameterSource`. Generated keys in Spring are supported with a `GeneratedKeyHolder`. The following is a complete example: + +```java + NamedParameterJdbcTemplate template = getTemplate(); // not shown + + List records = new ArrayList<>(); + GeneratedAlwaysRecord record = new GeneratedAlwaysRecord(); + record.setId(100); + record.setFirstName("Bob"); + record.setLastName("Jones"); + records.add(record); + + record = new GeneratedAlwaysRecord(); + record.setId(101); + record.setFirstName("Jim"); + record.setLastName("Smith"); + records.add(record); + + MultiRowInsertStatementProvider insertStatement = insertMultiple(records).into(generatedAlways) + .map(id).toProperty("id") + .map(firstName).toProperty("firstName") + .map(lastName).toProperty("lastName") + .build() + .render(RenderingStrategies.SPRING_NAMED_PARAMETER); + + SqlParameterSource parameterSource = new BeanPropertySqlParameterSource(insertStatement); + KeyHolder keyHolder = new GeneratedKeyHolder(); + + int rows = template.update(insertStatement.getInsertStatement(), parameterSource, keyHolder); + String firstGeneratedKey = (String) keyHolder.getKeyList().get(0).get("FULL_NAME"); + String secondGeneratedKey = (String) keyHolder.getKeyList().get(1).get("FULL_NAME"); +``` + +This can be simplified by using the utility class as follows: ```java - NamedParameterJdbcTemplate template = getTemplate(); + NamedParameterJdbcTemplate template = getTemplate(); // not shown + NamedParameterJdbcTemplateExtensions extensions = new NamedParameterJdbcTemplateExtensions(template); List records = new ArrayList<>(); GeneratedAlwaysRecord record = new GeneratedAlwaysRecord(); @@ -85,7 +275,39 @@ Batch insert support in Spring is a bit different than batch support in MyBatis3 record.setFirstName("Bob"); record.setLastName("Jones"); records.add(record); - + + record = new GeneratedAlwaysRecord(); + record.setId(101); + record.setFirstName("Jim"); + record.setLastName("Smith"); + records.add(record); + + Buildable> insertStatement = insertMultiple(records).into(generatedAlways) + .map(id).toProperty("id") + .map(firstName).toProperty("firstName") + .map(lastName).toProperty("lastName"); + + // no generated key retrieval + int rows = extensions.insertMultiple(insertStatement); + + // retrieve generated keys + KeyHolder keyHolder = new GeneratedKeyHolder(); + int rows = extensions.insertMultiple(insertStatement, keyHolder); +``` + +### Executing Batch Inserts +A JDBC batch insert is an efficient way to perform a bulk insert. It does not have the limitations of a multi-row insert and may perform better too. Spring does not support returning generated keys from a batch insert. The following is a complete example of a batch insert (note the use of `SqlParameterSourceUtils` to create an array of parameter sources from an array of input records): + +```java + NamedParameterJdbcTemplate template = getTemplate(); // not shown + + List records = new ArrayList<>(); + GeneratedAlwaysRecord record = new GeneratedAlwaysRecord(); + record.setId(100); + record.setFirstName("Bob"); + record.setLastName("Jones"); + records.add(record); + record = new GeneratedAlwaysRecord(); record.setId(101); record.setFirstName("Jim"); @@ -93,7 +315,7 @@ Batch insert support in Spring is a bit different than batch support in MyBatis3 records.add(record); SqlParameterSource[] batch = SqlParameterSourceUtils.createBatch(records.toArray()); - + BatchInsert batchInsert = insert(records) .into(generatedAlways) .map(id).toProperty("id") @@ -101,23 +323,92 @@ Batch insert support in Spring is a bit different than batch support in MyBatis3 .map(lastName).toProperty("lastName") .build() .render(RenderingStrategies.SPRING_NAMED_PARAMETER); - + int[] updateCounts = template.batchUpdate(batchInsert.getInsertStatementSQL(), batch); ``` -## Executing Delete and Update Statements -Updates and deletes use the `MapSqlParameterSource` as with select statements, but use the `update` method in the template. For example: +This can be simplified by using the utility class as follows: ```java - NamedParameterJdbcTemplate template = getTemplate(); + NamedParameterJdbcTemplate template = getTemplate(); // not shown + NamedParameterJdbcTemplateExtensions extensions = new NamedParameterJdbcTemplateExtensions(template); + + List records = new ArrayList<>(); + GeneratedAlwaysRecord record = new GeneratedAlwaysRecord(); + record.setId(100); + record.setFirstName("Bob"); + record.setLastName("Jones"); + records.add(record); + + record = new GeneratedAlwaysRecord(); + record.setId(101); + record.setFirstName("Jim"); + record.setLastName("Smith"); + records.add(record); + + Buildable> insertStatement = insertBatch(records) + .into(generatedAlways) + .map(id).toProperty("id") + .map(firstName).toProperty("firstName") + .map(lastName).toProperty("lastName"); + + int[] updateCounts = extensions.insertBatch(insertStatement); +``` + +## Executing Delete Statements +Delete statements use the `MapSqlParameterSource` as with select statements, but use the `update` method in the template. For example: + +```java + NamedParameterJdbcTemplate template = getTemplate(); // not shown + + DeleteStatementProvider deleteStatement = deleteFrom(generatedAlways) + .where(id, isLessThan(3)) + .build() + .render(RenderingStrategies.SPRING_NAMED_PARAMETER); + + SqlParameterSource parameterSource = new MapSqlParameterSource(deleteStatement.getParameters()); + + int rows = template.update(deleteStatement.getDeleteStatement(), parameterSource); +``` + +This can be simplified by using the utility class as follows: + +```java + NamedParameterJdbcTemplate template = getTemplate(); // not shown + NamedParameterJdbcTemplateExtensions extensions = new NamedParameterJdbcTemplateExtensions(template); + + Buildable deleteStatement = deleteFrom(generatedAlways) + .where(id, isLessThan(3)); + + int rows = extensions.delete(deleteStatement); +``` + +## Executing Update Statements +Update statements use the `MapSqlParameterSource` as with select statements, but use the `update` method in the template. For example: + +```java + NamedParameterJdbcTemplate template = getTemplate(); // not shown UpdateStatementProvider updateStatement = update(generatedAlways) .set(firstName).equalToStringConstant("Rob") - .where(id, isIn(1, 5, 22)) + .where(id, isIn(1, 5, 22)) .build() .render(RenderingStrategies.SPRING_NAMED_PARAMETER); - + SqlParameterSource parameterSource = new MapSqlParameterSource(updateStatement.getParameters()); - + int rows = template.update(updateStatement.getUpdateStatement(), parameterSource); ``` + +This can be simplified by using the utility class as follows: + +```java + NamedParameterJdbcTemplate template = getTemplate(); // not shown + NamedParameterJdbcTemplateExtensions extensions = new NamedParameterJdbcTemplateExtensions(template); + + Buildable updateStatement = update(generatedAlways) + .set(firstName).equalToStringConstant("Rob") + .where(id, isIn(1, 5, 22)); + + int rows = extensions.update(updateStatement); +``` diff --git a/src/site/markdown/docs/springBatch.md b/src/site/markdown/docs/springBatch.md index 79b8988f7..aca011c39 100644 --- a/src/site/markdown/docs/springBatch.md +++ b/src/site/markdown/docs/springBatch.md @@ -1,100 +1,164 @@ # Spring Batch Support This library provides some utilities to make it easier to interact with the MyBatis Spring Batch support. -## The Problem +MyBatis Spring provides support for interacting with Spring Batch (see +[http://www.mybatis.org/spring/batch.html](http://www.mybatis.org/spring/batch.html)). This support consists of +specialized implementations of Spring Batch's `ItemReader` and `ItemWriter` interfaces that have support for MyBatis +mappers. -MyBatis Spring support provides utility classes for interacting with Spring Batch (see [http://www.mybatis.org/spring/batch.html](http://www.mybatis.org/spring/batch.html)). These classes are specialized implementations of Spring Batch's `ItemReader` and `ItemWriter` interfaces that have support for MyBatis mappers. +The `ItemWriter` implementation works with SQL generated by MyBatis Dynamic SQL with no modification needed. -The `ItemWriter` implementations work with SQL generated by MyBatis Dynamic SQL with no modification needed. +The `ItemReader` implementations need special care. Those classes assume that all query parameters will be placed in a +Map (as per usual when using multiple parameters in a query). MyBatis Dynamic SQL, by default, builds a parameter +object that is intended to be the only parameter for a query. The library contains utilities for overcoming this +difficulty. -The `ItemReader` implementations need special care. Those classes assume that all query parameters will be placed in a Map (as per usual when using multiple parameters in a query). MyBatis Dynamic SQL, by default, builds a parameter object that should be the only parameter in a query and will not work when placed in a Map of parameters. +## Using MyBatisCursorItemReader -## The Solution +The `MyBatisCursorItemReader` class works with built-in support for cursor based queries in MyBatis. Queries of this +type will read row by row and MyBatis will convert each result row to a result object without having to read the entire +result set into memory. The normal rendering for MyBatis will work for queries using this reader, but special care +must be taken to prepare the parameter values for use with this reader. See the following example: -The solution involves these steps: - -1. The SQL must be rendered such that the parameter markers are aware of the enclosing parameter Map in the `ItemReader` -1. The `SelectStatementProvider` must be placed in the `ItemReader` parameter Map with a known key. -1. The `@SelectProvider` must be configured to be aware of the enclosing parameter Map +```java +@Bean +public MyBatisCursorItemReader reader(SqlSessionFactory sqlSessionFactory) { + SelectStatementProvider selectStatement = select(person.allColumns()) + .from(person) + .where(lastName, isEqualTo("flintstone")) + .build() + .render(RenderingStrategies.MYBATIS3); + + MyBatisCursorItemReader reader = new MyBatisCursorItemReader<>(); + reader.setQueryId(PersonMapper.class.getName() + ".selectMany"); + reader.setSqlSessionFactory(sqlSessionFactory); + reader.setParameterValues(SpringBatchUtility.toParameterValues(selectStatement)); + return reader; +} +``` -MyBatis Dynamic SQL provides utilities for each of these requirements. Each utility uses a shared Map key for consistency. +Note the use of `SpringBatchUtility.toParameterValues(...)`. This utility will set up the parameter Map correctly for the +rendered statement, and for use with a library supplied `@selectProvider`. See the following for an example of the mapper +method used for the query coded above: -## Spring Batch Item Readers +```java +@Mapper +public interface PersonMapper { + + @SelectProvider(type=SpringBatchProviderAdapter.class, method="select") + @Results({ + @Result(column="id", property="id", id=true), + @Result(column="first_name", property="firstName"), + @Result(column="last_name", property="lastName") + }) + List selectMany(Map parameterValues); +} +``` -MyBatis Spring support supplies two implementations of the `ItemReader` interface: +Note the use of the `SpringBatchProviderAdapter` - that adapter knows how to retrieve the rendered queries from the +parameter map initialed in the method above. -1. `org.mybatis.spring.batch.MyBatisCursorItemReader` - for queries that can be efficiently processed through a single select statement and a cursor -1. `org.mybatis.spring.batch.MyBatisPagingItemReader` - for queries that should be processed as a series of paged selects. Note that MyBatis does not provide any native support for paged queries - it is up to the user to write SQL for paging. The `MyBatisPagingItemWriter` simply makes properties available that specify which page should be read currently. +### Migrating from 1.x Support for MyBatisCursorItemReader -MyBatis Dynamic SQL supplies specialized select statements that will render properly for the different implementations of `ItemReader`: +In version 1.x, the library supplied a special utility for creating a select statement as follows: -1. `SpringBatchUtility.selectForCursor(...)` will create a select statement that is appropriate for the `MyBatisCursorItemReader` - a single select statement that will be read with a cursor -1. `SpringBatchUtility.selectForPaging(...)` will create a select statement that is appropriate for the `MyBatisPagingItemReader` - a select statement that will be called multiple times - one for each page as configured on the batch job. +```java +SelectStatementProvider selectStatement = SpringBatchUtility.selectForCursor(person.allColumns()) + .from(person) + .where(lastName, isEqualTo("flintstone")) + .build() + .render(); +``` -**Very Important:** The paging implementation will only work for databases that support limit and offset in select statements. Fortunately, most databases do support this - with the notable exception of Oracle. +That utility method was limited in capability and has been removed. The new method described above allows the full +capabilities of the library. To migrate, follow these steps: +1. Replace `SpringBatchUtility.selectForCursor(...)` with `SqlBuilder.select(...)` +2. Replace `render()` with `render(RenderingStrategies.MYBATIS3)` -### Rendering for Cursor +## Using MyBatisPagingItemReader -Queries intended for the `MyBatisCursorItemReader` should be rendered as follows: +The `MyBatisPagingItemReader` class works with paging queries - queries that read rows in pages and process page by page +rather than row by row. The normal rendering for MyBatis will work NOT for queries using this reader because MyBatis +Spring support supplies specially named parameters for page size, offset, etc. So the query must be rendered properly +to respond to these parameter values that are supplied at runtime. As with the other reader, special care +must also be taken to prepare the parameter values for use with this reader. See the following example: ```java - SelectStatementProvider selectStatement = SpringBatchUtility.selectForCursor(person.allColumns()) - .from(person) - .where(lastName, isEqualTo("flintstone")) - .build() - .render(); // renders for MyBatisCursorItemReader +@Bean +public MyBatisPagingItemReader reader(SqlSessionFactory sqlSessionFactory) { + SelectStatementProvider selectStatement = select(person.allColumns()) + .from(person) + .where(forPagingTest, isEqualTo(true)) + .orderBy(id) + .limit(SpringBatchUtility.MYBATIS_SPRING_BATCH_PAGESIZE) + .offset(SpringBatchUtility.MYBATIS_SPRING_BATCH_SKIPROWS) + .build() + .render(SpringBatchUtility.SPRING_BATCH_PAGING_ITEM_READER_RENDERING_STRATEGY); + + MyBatisPagingItemReader reader = new MyBatisPagingItemReader<>(); + reader.setQueryId(PersonMapper.class.getName() + ".selectMany"); + reader.setSqlSessionFactory(sqlSessionFactory); + reader.setParameterValues(SpringBatchUtility.toParameterValues(selectStatement)); + reader.setPageSize(7); + return reader; +} ``` - -### Rendering for Paging - -Queries intended for the `MyBatisPagingItemReader` should be rendered as follows: +Notice the following important items: + +1. The `limit` and `offset` methods in the query are used to set up paging support in the query. With MyBatis Spring + batch support, the integration library will supply values for those parameters at runtime. Any values you code in the + select statement will be ignored - only the values supplied by the library will be used. We supply two constants + to make this clearer: `MYBATIS_SPRING_BATCH_PAGESIZE` and `MYBATIS_SPRING_BATCH_SKIPROWS`. You can use these values + to make the code clearer, but again the values will be ignored at runtime. +2. The query must be rendered with the `SPRING_BATCH_PAGING_ITEM_READER_RENDERING_STRATEGY` rendering strategy. This + rendering strategy will render the query so that it will respond properly to the runtime values supplied for page size + and skip rows. +3. Note the use of `SpringBatchUtility.toParameterValues(...)`. This utility will set up the parameter Map correctly for + the rendered statement, and for use with a library supplied `@selectProvider`. See the following for an example of + the mapper method used for the query coded above: ```java - SelectStatementProvider selectStatement = SpringBatchUtility.selectForPaging(person.allColumns()) - .from(person) - .where(lastName, isEqualTo("flintstone")) - .build() - .render(); // renders for MyBatisPagingItemReader +@Mapper +public interface PersonMapper { + + @SelectProvider(type=SpringBatchProviderAdapter.class, method="select") + @Results({ + @Result(column="id", property="id", id=true), + @Result(column="first_name", property="firstName"), + @Result(column="last_name", property="lastName") + }) + List selectMany(Map parameterValues); +} ``` -## Creating the Parameter Map - -The `SpringBatchUtility` provides a method to create the parameter values Map needed by the MyBatis Spring `ItemReader` implementations. It can be used as follows: +Note the use of the `SpringBatchProviderAdapter` - that adapter knows how to retrieve the rendered queries from the +parameter map initialed in the method above. -For cursor based queries... +### Migrating from 1.x Support for MyBatisPagingItemReader -```java - MyBatisCursorItemReader reader = new MyBatisCursorItemReader<>(); - reader.setQueryId(PersonMapper.class.getName() + ".selectMany"); - reader.setSqlSessionFactory(sqlSessionFactory); - reader.setParameterValues(SpringBatchUtility.toParameterValues(selectStatement)); // create parameter map -``` -For paging based queries... +In version 1.x, the library supplied a special utility for creating a select statement as follows: ```java - MyBatisPagingItemReader reader = new MyBatisPagingItemReader<>(); - reader.setQueryId(PersonMapper.class.getName() + ".selectMany"); - reader.setSqlSessionFactory(sqlSessionFactory); - reader.setPageSize(7); - reader.setParameterValues(SpringBatchUtility.toParameterValues(selectStatement)); // create parameter map +SelectStatementProvider selectStatement = SpringBatchUtility.selectForPaging(person.allColumns()) + .from(person) + .where(forPagingTest, isEqualTo(true)) + .orderBy(id) + .build() + .render(); ``` +That utility method was very limited in capability and has been removed. The prior method only supported limit and +offset based queries - which are not supported in all databases. The new method described above allows the full +capabilities of the library to be used. To migrate, follow these steps: -## Specialized @SelectProvider Adapter +1. Replace `SpringBatchUtility.selectForPaging(...)` with `SqlBuilder.select(...)` +2. Add `limit()`, `fetchFirst()`, and `offset()` method calls as appropriate for your query and database +3. Replace `render()` with `render(RenderingStrategies.SPRING_BATCH_PAGING_ITEM_READER_RENDERING_STRATEGY)` -MyBatis mapper methods should be configured to use the specialized `@SelectProvider` adapter as follows: - -```java - @SelectProvider(type=SpringBatchProviderAdapter.class, method="select") // use the Spring batch adapter - @Results({ - @Result(column="id", property="id", id=true), - @Result(column="first_name", property="firstName"), - @Result(column="last_name", property="lastName") - }) - List selectMany(Map parameterValues); -``` -## Complete Example +## Complete Examples -The unit tests for MyBatis Dynamic SQL include a complete examples of using MyBatis Spring Batch support using the MyBatis supplied reader as well as both types of MyBatis supplied writers. You can see the full example here: [https://github.com/mybatis/mybatis-dynamic-sql/tree/master/src/test/java/examples/springbatch](https://github.com/mybatis/mybatis-dynamic-sql/tree/master/src/test/java/examples/springbatch) +The unit tests for MyBatis Dynamic SQL include a complete example of using MyBatis Spring Batch support using the +MyBatis supplied reader as well as both types of MyBatis supplied writers. You can see the full example +here: [https://github.com/mybatis/mybatis-dynamic-sql/tree/master/src/test/java/examples/springbatch](https://github.com/mybatis/mybatis-dynamic-sql/tree/master/src/test/java/examples/springbatch) diff --git a/src/site/markdown/docs/subQueries.md b/src/site/markdown/docs/subQueries.md new file mode 100644 index 000000000..0f2913093 --- /dev/null +++ b/src/site/markdown/docs/subQueries.md @@ -0,0 +1,392 @@ +# Subquery Support +The library currently supports subqueries in the following areas: + +1. In where clauses - both with the "exists" operator and with column-based conditions +1. In certain insert statements +1. In update statements +1. In the "from" clause of a select statement +1. In join clauses of a select statement + +Before we show examples of subqueries, it is important to understand how the library generates and applies +table qualifiers in select statements. We'll cover that first. + +## Table Qualifiers in Select Statements + +The library attempts to automatically calculate table qualifiers. If a table qualifier is specified, +the library will automatically render the table qualifier on all columns associated with the +table. For example with the following query: + +```java +SelectStatementProvider selectStatement = + select(id, animalName) + .from(animalData, "ad") + .build() + .render(RenderingStrategies.MYBATIS3); +``` + +The library will render SQL as: + +```sql +select ad.id, ad.animal_name +from AnimalData ad +``` + +Notice that the table qualifier `ad` is automatically applied to columns in the select list. + +In the case of join queries the table qualifier specified, or if not specified the table name +itself, will be used as the table qualifier. However, this function is disabled for joins on subqueries. + +With subqueries, it is important to understand the limits of automatic table qualifiers. The rules are +as follows: + +1. The scope of automatic table qualifiers is limited to a single select statement. For subqueries, the outer + query has a different scope than the subquery. +1. A qualifier can be applied to a subquery, but that qualifier is not automatically applied to + any column + +As an example, consider the following query: + +```java +DerivedColumn rowNum = DerivedColumn.of("rownum()"); + +SelectStatementProvider selectStatement = + select(animalName, rowNum) + .from( + select(id, animalName) + .from(animalData, "a") + .where(id, isLessThan(22)) + .orderBy(animalName.descending()), + "b" + ) + .where(rowNum, isLessThan(5)) + .and(animalName, isLike("%a%")) + .build() + .render(RenderingStrategies.MYBATIS3); +``` + +The rendered SQL will be as follows: + +```sql +select animal_name, rownum() +from (select a.id, a.animal_name + from AnimalDate a + where id < #{parameters.p1} + order by animal_name desc) b +where rownum() < #{parameters.p2} + and animal_name like #{parameters.p3} +``` + +Notice that the qualifier `a` is automatically applied to columns in the subquery and that the +qualifier `b` is not applied anywhere. + +If your query requires the subquery qualifier to be applied to columns in the outer select list, +you can manually apply the qualifier to columns as follows: + +```java +DerivedColumn rowNum = DerivedColumn.of("rownum()"); + +SelectStatementProvider selectStatement = + select(animalName.qualifiedWith("b"), rowNum) + .from( + select(id, animalName) + .from(animalData, "a") + .where(id, isLessThan(22)) + .orderBy(animalName.descending()), + "b" + ) + .where(rowNum, isLessThan(5)) + .and(animalName.qualifiedWith("b"), isLike("%a%")) + .build() + .render(RenderingStrategies.MYBATIS3); +``` + +In this case, we have manually applied the qualifier `b` to columns in the outer query. The +rendered SQL looks like this: + +```sql +select b.animal_name, rownum() +from (select a.id, a.animal_name + from AnimalDate a + where id < #{parameters.p1} + order by animal_name desc) b +where rownum() < #{parameters.p2} + and b.animal_name like #{parameters.p3} +``` + +## Subqueries in Where Conditions +The library support subqueries in the following where conditions: + +- exists +- notExists +- isEqualTo +- isNotEqualTo +- isIn +- isNotIn +- isGreaterThan +- isGreaterThanOrEqualTo +- isLessThan +- isLessThanOrEqualTo + +An example of an exists subquery is as follows: + +```java +SelectStatementProvider selectStatement = select(itemMaster.allColumns()) + .from(itemMaster, "im") + .where(exists( + select(orderLine.allColumns()) + .from(orderLine, "ol") + .where(orderLine.itemId, isEqualTo(itemMaster.itemId)) + )) + .orderBy(itemMaster.itemId) + .build() + .render(RenderingStrategies.MYBATIS3); +``` + +Note that the qualifier for the outer query ("im") is automatically applied to the inner query, as well as the +qualifier for the inner query ("ol"). Carrying alias from an outer query to an inner query is only supported with +exists or not exists sub queries. + +An example of a column based subquery is as follows: + +```java +SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight) + .from(animalData) + .where(brainWeight, isEqualTo( + select(min(brainWeight)) + .from(animalData) + ) + ) + .orderBy(animalName) + .build() + .render(RenderingStrategies.MYBATIS3); +``` + +### Kotlin Support +The library includes Kotlin versions of the where conditions that allow use of the Kotlin subquery builder. The Kotlin +where conditions are in the `org.mybatis.dynamic.sql.util.kotlin` package. + +An example of an exists subquery is as follows: +```kotlin +val selectStatement = select(ItemMaster.allColumns()) { + from(ItemMaster, "im") + where { + exists { + select(OrderLine.allColumns()) { + from(OrderLine, "ol") + where { OrderLine.itemId isEqualTo ItemMaster.itemId } + } + } + orderBy(ItemMaster.itemId) + } +} +``` + +An example of a column based subquery is as follows: +```kotlin +val selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) { + from(Person) + where { + id isEqualTo { + select(max(id)) { + from(Person) + } + } + } +} +``` + +## Subqueries in Insert Statements +The library supports an INSERT statement that retrieves values from a SELECT statement. For example: + +```java +InsertSelectStatementProvider insertSelectStatement = insertInto(animalDataCopy) + .withColumnList(id, animalName, bodyWeight, brainWeight) + .withSelectStatement( + select(id, animalName, bodyWeight, brainWeight) + .from(animalData) + .where(id, isLessThan(22)) + ) + .build() + .render(RenderingStrategies.MYBATIS3); +``` + +### Kotlin Support + +The library includes a Kotlin builder for subqueries in insert statements that integrates with the select DSL. You +can write inserts like this: + +```kotlin +val insertStatement = insertSelect(Person) { + columns(id, firstName, lastName, birthDate, employed, occupation, addressId) + select(add(id, constant("100")), firstName, lastName, birthDate, employed, occupation, addressId) { + from(Person) + orderBy(id) + } +} +``` + +## Subqueries in Update Statements +The library supports setting update values based on the results of a subquery. For example: + +```java +UpdateStatementProvider updateStatement = update(animalData) + .set(brainWeight).equalTo( + select(avg(brainWeight)) + .from(animalData) + .where(brainWeight, isGreaterThan(22.0)) + ) + .where(brainWeight, isLessThan(1.0)) + .build() + .render(RenderingStrategies.MYBATIS3); +``` + +### Kotlin Support +The library includes a Kotlin builder for subqueries in update statements that integrates +with the select DSL. You can write subqueries like this: + +```kotlin +val updateStatement = update(Person) { + set(addressId) equalToQueryResult { + select(add(max(addressId), constant("1"))) { + from(Person) + } + } + where { id isEqualTo 3 } +} +``` + +Note the Kotlin method name is `set(xxx).equalToQueryResult(...)` - this is to avoid a collison with +other methods in the update DSL. + +## Subqueries in a From Clause + +The library supports subqueries in from clauses and the syntax is a natural extension of the +select DSL. An example is as follows: + +```java +DerivedColumn rowNum = DerivedColumn.of("rownum()"); + +SelectStatementProvider selectStatement = + select(animalName, rowNum) + .from( + select(id, animalName) + .from(animalData) + .where(id, isLessThan(22)) + .orderBy(animalName.descending()) + ) + .where(rowNum, isLessThan(5)) + .and(animalName, isLike("%a%")) + .build() + .render(RenderingStrategies.MYBATIS3); +``` + +Notice the use of a `DerivedColumn` to easily specify a function like `rownum()` that can be +used both in the select list and in a where condition. + +### Kotlin Support + +The library includes a Kotlin builder for subqueries that integrates with the select DSL. You +can write queries like this: + +```kotlin +val selectStatement = + select(firstName, rowNum) { + from { + select(id, firstName) { + from(Person) + where { id isLessThan 22 } + orderBy(firstName.descending()) + } + } + where { rowNum isLessThan 5 } + and { firstName isLike "%a%" } + } +``` + +The same rules about table qualifiers apply as stated above. In Kotlin, a subquery qualifier +can be added with the overloaded "+" operator as shown below: + +```kotlin +val selectStatement = + select(firstName, rowNum) { + from { + select(id, firstName) { + from(Person, "a") + where { id isLessThan 22 } + orderBy(firstName.descending()) + } + + "b" + } + where { rowNum isLessThan 5 } + and { firstName isLike "%a%" } + } +``` + +In this case the `a` qualifier is used in the context of the inner select statement and +the `b` qualifier is applied to the subquery as a whole. + +## Subqueries in Join Clauses +The library supports subqueries in "join" clauses similarly to subqueries in "from" clauses. For example: + +```java +SelectStatementProvider selectStatement = select(orderMaster.orderId, orderMaster.orderDate, + orderDetail.lineNumber, orderDetail.description, orderDetail.quantity) + .from(orderMaster, "om") + .join( + select(orderDetail.orderId, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity) + .from(orderDetail), + "od") + .on(orderMaster.orderId, equalTo(orderDetail.orderId.qualifiedWith("od"))) + .build() + .render(RenderingStrategies.MYBATIS3); +``` + +This is rendered as: + +```sql +select om.order_id, om.order_date, line_number, description, quantity +from OrderMaster om +join (select order_id, line_number, description, quantity from OrderDetail) od +on om.order_id = od.order_id +``` + +Notice that the subquery is aliased with "od", but that alias is not automatically applied so it must +be specified when required. If in doubt, specify the alias with the `qualifiedBy` method. + +### Kotlin Support +The Kotlin select build supports subqueries in joins as follows: + +```kotlin +val selectStatement = select(OrderLine.orderId, OrderLine.quantity, + "im"(ItemMaster.itemId), ItemMaster.description) { + from(OrderMaster, "om") + join(OrderLine, "ol") { + on(OrderMaster.orderId) equalTo OrderLine.orderId + } + leftJoin( + { + select(ItemMaster.allColumns()) { + from(ItemMaster) + } + + "im" + } + ) { + on(OrderLine.itemId) equalTo "im"(ItemMaster.itemId) + } + orderBy(OrderLine.orderId, ItemMaster.itemId) +} +``` + +This is rendered as: + +```sql +select ol.order_id, ol.quantity, im.item_id, description +from OrderMaster om join OrderLine ol on om.order_id = ol.order_id +left join (select * from ItemMaster) im on ol.item_id = im.item_id +order by order_id, item_id +``` + +Notice again that sub query qualifiers must be specified when needed. In this case we use a Kotlin trick - an invoke +operator function that gets close to natural SQL syntax (```"im"(ItemMaster.itemId)```). Also note that the Kotlin +join methods accept two lambda functions - one for the sub query and one for the join specification. Only the join +specification can be outside the parenthesis of the join methods. diff --git a/src/site/markdown/docs/update.md b/src/site/markdown/docs/update.md index 67f00257c..c98388ef6 100644 --- a/src/site/markdown/docs/update.md +++ b/src/site/markdown/docs/update.md @@ -3,7 +3,7 @@ Update statements are composed by specifying the table and columns to update, an ```java UpdateStatementProvider updateStatement = update(animalData) - .set(bodyWeight).equalTo(record.getBodyWeight()) + .set(bodyWeight).equalTo(row.getBodyWeight()) .set(animalName).equalToNull() .where(id, isIn(1, 5, 7)) .or(id, isIn(2, 6, 8), and(animalName, isLike("%bat"))) @@ -32,7 +32,7 @@ For example: ```java UpdateStatementProvider updateStatement = update(animalData) - .set(bodyWeight).equalTo(record.getBodyWeight()) + .set(bodyWeight).equalTo(row.getBodyWeight()) .set(animalName).equalToNull() .build() .render(RenderingStrategies.MYBATIS3); @@ -42,7 +42,7 @@ For example: The UpdateStatementProvider object can be used as a parameter to a MyBatis mapper method directly. If you are using an annotated mapper, the update method should look like this: - + ```java import org.apache.ibatis.annotations.UpdateProvider; import org.mybatis.dynamic.sql.update.render.UpdateStatementProvider; @@ -59,7 +59,7 @@ import org.mybatis.dynamic.sql.util.SqlProviderAdapter; We do not recommend using an XML mapper for update statements, but if you want to do so the UpdateStatementProvider object can be used as a parameter to a MyBatis mapper method directly. If you are using an XML mapper, the update method should look like this in the Java interface: - + ```java import org.mybatis.dynamic.sql.update.render.UpdateStatementProvider; diff --git a/src/site/markdown/docs/whereClauses.md b/src/site/markdown/docs/whereClauses.md index 4933f9ec7..a7e5516e5 100644 --- a/src/site/markdown/docs/whereClauses.md +++ b/src/site/markdown/docs/whereClauses.md @@ -69,7 +69,10 @@ Most of the conditions also support a subquery. For example: ``` ## Stand Alone Where Clauses -You can use the where clause support on its own if you would rather code your own SQL for the remainder of a statement. There may be several reasons to do this - mainly if the library doesn't support some SQL or MyBatis feature you want to use. A good example would be paginated queries which are currently not support by the library. If you want to use a stand alone where clause, you can code a mapper method that looks like this: +Although rare, you can use the where clause support on its own if you would rather code your own SQL for the remainder +of a statement. There may be several reasons to do this - mainly if the library doesn't support some SQL or MyBatis +feature you want to use. A good example would be if you want to append other SQL to the generated SQL produced by the +library. If you want to use a stand alone where clause, you can code a mapper method that looks like this: ```java @Select({ @@ -78,22 +81,24 @@ You can use the where clause support on its own if you would rather code your ow "${whereClause}" }) @ResultMap("AnimalDataResult") - List selectByExample(WhereClauseProvider whereClause); + List selectWithWhereClause(WhereClauseProvider whereClause); ``` You can build a stand alone where clause and call your mapper like this: ```java - WhereClauseProvider whereClause = where(id, isNotBetween(10).and(60)) + Optional whereClause = where(id, isNotBetween(10).and(60)) .build() .render(RenderingStrategies.MYBATIS3); - List animals = mapper.selectByExample(whereClause); + List animals = whereClause.map(wc -> mapper.selectWithWhereClause(wc)).orElse(Collections.emptyList()); ``` -This method works well when there are no other parameters needed for the statement and when there are no table aliases involved. If you have those other needs, then see the following. +This method works well when there are no other parameters needed for the statement and when there are no table aliases +involved. If you have those other needs, then see the following. ### Table Aliases -If you need to use a table alias in the generated where clause you can supply it on the render method using the `TableAliasCalculator` class. For example, if you have a mapper like this: +If you need to use a table alias in the generated where clause you can supply it on the render method using the +`TableAliasCalculator` class. For example, if you have a mapper like this: ```java @Select({ @@ -102,21 +107,24 @@ If you need to use a table alias in the generated where clause you can supply it "${whereClause}" }) @ResultMap("AnimalDataResult") - List selectByExampleWithAlias(WhereClauseProvider whereClause); + List selectWithWhereClauseAndAlias(WhereClauseProvider whereClause); ``` Then you can specify the alias for the generated WHERE clause on the render method like this: ```java - WhereClauseProvider whereClause = where(id, isEqualTo(1), or(bodyWeight, isGreaterThan(1.0))) + Optional whereClause = where(id, isEqualTo(1), or(bodyWeight, isGreaterThan(1.0))) .build() .render(RenderingStrategies.MYBATIS3, TableAliasCalculator.of(animalData, "a")); - List animals = mapper.selectByExampleWithAlias(whereClause); + List animals = whereClause.map(wc -> mapper.selectWithWhereClauseAndAlias(wc)).orElse(Collections.emptyList()); ``` -It is more likely that you will be using table aliases with hand coded joins where there is more than on table alias. In this case, you supply a `Map` to the TableAliasCalculator that holds an alias for each table involved in the WHERE clause. +It is more likely that you will be using table aliases with hand coded joins where there is more than on table alias. +In this case, you supply a `Map` to the TableAliasCalculator that holds an alias for each table +involved in the WHERE clause. ### Handling Multiple Parameters -By default, the WHERE clause renderer assumes that the rendered WHERE clause will be the only parameter to the mapper method. This is not always the case. For example, suppose you have a paginated query like this (this is HSQLDB syntax): +By default, the WHERE clause renderer assumes that the rendered WHERE clause will be the only parameter to the mapper +method. This is not always the case. For example, suppose you have a paginated query like this (this is HSQLDB syntax): ```java @Select({ @@ -127,19 +135,21 @@ By default, the WHERE clause renderer assumes that the rendered WHERE clause wil "OFFSET #{offset,jdbcType=INTEGER} LIMIT #{limit,jdbcType=INTEGER}" }) @ResultMap("AnimalDataResult") - List selectByExampleWithLimitAndOffset(@Param("whereClauseProvider") WhereClauseProvider whereClause, + List selectWithWhereClauseLimitAndOffset(@Param("whereClauseProvider") WhereClauseProvider whereClause, @Param("limit") int limit, @Param("offset") int offset); ``` -In this mapper method there are three parameters. So in this case it will be necessary to tell the WHERE rendered what parameter name to use the for rendered where clause. That code looks like this: +In this mapper method there are three parameters. So in this case it will be necessary to tell the WHERE rendered what +parameter name to use the for rendered where clause. That code looks like this: ```java - WhereClauseProvider whereClause = where(id, isLessThan(60)) + Optional whereClause = where(id, isLessThan(60)) .build() .render(RenderingStrategies.MYBATIS3, "whereClauseProvider"); - - List animals = mapper.selectByExampleWithLimitAndOffset(whereClause, 5, 15); + + List animals = whereClause.map(wc -> mapper.selectWithWhereClauseLimitAndOffset(wc, 5, 15)).orElse(Collections.emptyList()); ``` -Notice that the string `whereClauseProvider` is used both as the parameter name in the mapper `@Param` annotation and the parameter name in the `render` method. +Notice that the string `whereClauseProvider` is used both as the parameter name in the mapper `@Param` annotation, +and the parameter name in the `render` method. -The render method also has an override that accepts a TableAliasCalculator and a parameter name. +The render method also has an override that accepts a `TableAliasCalculator` and a parameter name. diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md index 1fa622d69..9d1ca46b6 100644 --- a/src/site/markdown/index.md +++ b/src/site/markdown/index.md @@ -1,7 +1,9 @@ # MyBatis Dynamic SQL -MyBatis Dynamic SQL is an SQL templating library that makes it easier to execute dynamic SQL with MyBatis. It generates SQL that is formatted in such a way that it can be directly executed by MyBatis. +MyBatis Dynamic SQL is an SQL DSL (domain specific language). It allows developers to write SQL in Java or Kotlin using the natural feel of native SQL. It also +includes many functions for creating very dynamic SQL statements based on current runtime parameter values. -The library also supports generating SQL that is formatted for use by Spring JDBC Templates. +The DSL will render standard SQL DELETE, INSERT, SELECT, and UPDATE statements - and associated +parameters - that can be used directly by SQL execution engines like MyBatis or Spring JDBC template. Please read the user's guide for detailed instructions on use. The user's guide is accessible through menu links to the left. diff --git a/src/site/resources/css/site.css b/src/site/resources/css/site.css index 86104a8d0..54e023e50 100644 --- a/src/site/resources/css/site.css +++ b/src/site/resources/css/site.css @@ -1,11 +1,11 @@ /** - * Copyright 2016-2017 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/site/site.xml b/src/site/site.xml index d5a98930b..86b32feac 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -1,13 +1,13 @@ diff --git a/src/test/resources/mybatis-dynamic-sql.properties b/src/test/resources/mybatis-dynamic-sql.properties new file mode 100644 index 000000000..fc8f38834 --- /dev/null +++ b/src/test/resources/mybatis-dynamic-sql.properties @@ -0,0 +1,17 @@ +# +# Copyright 2016-2025 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# 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. +# + +nonRenderingWhereClauseAllowed=false diff --git a/travis/after_success.sh b/travis/after_success.sh deleted file mode 100644 index 1bf631423..000000000 --- a/travis/after_success.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/bash -# -# Copyright 2016-2019 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - - -# Get Commit Message -commit_message=$(git log --format=%B -n 1) -echo "Current commit detected: ${commit_message}" - -# We build for several JDKs on Travis. -# Some actions, like analyzing the code (Coveralls) and uploading -# artifacts on a Maven repository, should only be made for one version. - -# If the version is 1.8, then perform the following actions. -# 1. Notify Coveralls. -# a. Use -q option to only display Maven errors and warnings. -# If this is a build on the master branch and not a pull request then -# 2. Upload artifacts to Sonatype. -# a. Use -q option to only display Maven errors and warnings. -# b. Use --settings to force the usage of our "settings.xml" file. - -if [ $TRAVIS_JDK_VERSION == "openjdk8" ] && [ $TRAVIS_REPO_SLUG == "mybatis/mybatis-dynamic-sql" ]; then - - ./mvnw clean test jacoco:report coveralls:report -q - echo -e "Successfully ran coveralls under Travis job ${TRAVIS_JOB_NUMBER}" - - if [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_BRANCH" == "master" ] && [[ "$commit_message" != *"[maven-release-plugin]"* ]]; then - # Run Sonar Analysis - ./mvnw sonar:sonar -Dsonar.projectKey=mybatis_mybatis-dynamic-sql - # Deploy to Sonatype - ./mvnw clean deploy -q --settings ./travis/settings.xml - echo -e "Successfully deployed SNAPSHOT artifacts to Sonatype under Travis job ${TRAVIS_JOB_NUMBER}" - else - echo "Not a master branch commit so no deployment of the new snapshot needed" - fi -else - echo "Travis Pull Request: $TRAVIS_PULL_REQUEST" - echo "Travis Branch: $TRAVIS_BRANCH" - echo "Travis build skipped" -fi