diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 43119e8b4..e89bd81b6 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -7,9 +7,10 @@ jobs:
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
+        cache: [maven]
+        distribution: [temurin]
+        java: [17, 21, 23, 24-ea]
         os: [ubuntu-latest]
-        java: [17, 21, 22-ea]
-        distribution: ['temurin']
       fail-fast: false
       max-parallel: 4
     name: Test JDK ${{ matrix.java }}, ${{ matrix.os }}
@@ -21,5 +22,6 @@ jobs:
         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/coveralls.yaml b/.github/workflows/coveralls.yaml
index a898e3793..270a6c85b 100644
--- a/.github/workflows/coveralls.yaml
+++ b/.github/workflows/coveralls.yaml
@@ -11,8 +11,9 @@ jobs:
       - name: Set up JDK
         uses: actions/setup-java@v4
         with:
+          cache: maven
+          distribution: temurin
           java-version: 21
-          distribution: zulu
       - 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
diff --git a/.github/workflows/site.yaml b/.github/workflows/site.yaml
index 746afc0ec..5d6998019 100644
--- a/.github/workflows/site.yaml
+++ b/.github/workflows/site.yaml
@@ -14,21 +14,18 @@ jobs:
       - name: Set up JDK
         uses: actions/setup-java@v4
         with:
+          cache: maven
+          distribution: temurin
           java-version: 21
-          distribution: zulu
-      - uses: webfactory/ssh-agent@master
-        with:
-          ssh-private-key: ${{ secrets.DEPLOY_KEY }}
       - name: Build site
-        run: ./mvnw site site:stage -DskipTests -B -V --no-transfer-progress -Dlicense.skip=true
+        run: ./mvnw site site:stage -DskipTests -Dlicense.skip=true -B -V --no-transfer-progress --settings ./.mvn/settings.xml
         env:
           CI_DEPLOY_USERNAME: ${{ secrets.CI_DEPLOY_USERNAME }}
           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.6.1
+        uses: JamesIves/github-pages-deploy-action@v4
         with:
-          ssh-key: true
           branch: gh-pages
           folder: target/staging
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          ssh-key: ${{ secrets.DEPLOY_KEY }}
diff --git a/.github/workflows/sonar.yaml b/.github/workflows/sonar.yaml
index e4bf41ac8..293c0e4ad 100644
--- a/.github/workflows/sonar.yaml
+++ b/.github/workflows/sonar.yaml
@@ -17,10 +17,11 @@ jobs:
       - name: Set up JDK
         uses: actions/setup-java@v4
         with:
+          cache: maven
+          distribution: temurin
           java-version: 21
-          distribution: zulu
       - name: Analyze with SonarCloud
-        run: ./mvnw verify jacoco:report sonar:sonar -B -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
+        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
index 72086bc1d..494bb2f4c 100644
--- a/.github/workflows/sonatype.yaml
+++ b/.github/workflows/sonatype.yaml
@@ -14,8 +14,9 @@ jobs:
       - name: Set up JDK
         uses: actions/setup-java@v4
         with:
+          cache: maven
+          distribution: temurin
           java-version: 21
-          distribution: zulu
       - name: Deploy to Sonatype
         run: ./mvnw deploy -DskipTests -B -V --no-transfer-progress --settings ./.mvn/settings.xml -Dlicense.skip=true
         env:
diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml
index 2f7f4c3a1..93acda146 100644
--- a/.mvn/extensions.xml
+++ b/.mvn/extensions.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
 
-       Copyright 2016-2024 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.
diff --git a/.mvn/settings.xml b/.mvn/settings.xml
index 9987bedc4..af6e63413 100644
--- a/.mvn/settings.xml
+++ b/.mvn/settings.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
 
-       Copyright 2016-2024 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.
diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java
index f6cb0fa0b..bdf0ddfa6 100644
--- a/.mvn/wrapper/MavenWrapperDownloader.java
+++ b/.mvn/wrapper/MavenWrapperDownloader.java
@@ -30,7 +30,7 @@
 import java.util.concurrent.ThreadLocalRandom;
 
 public final class MavenWrapperDownloader {
-    private static final String WRAPPER_VERSION = "3.3.1";
+    private static final String WRAPPER_VERSION = "3.3.2";
 
     private static final boolean VERBOSE = Boolean.parseBoolean(System.getenv("MVNW_VERBOSE"));
 
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
index 34d543889..01aa6654c 100644
--- a/.mvn/wrapper/maven-wrapper.properties
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -14,6 +14,7 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-wrapperVersion=3.3.1
-distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip
-wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.1/maven-wrapper-3.3.1.jar
+wrapperVersion=3.3.2
+distributionType=source
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d80d14ee3..c65f2c09c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,112 @@
 
 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 <T> foo(column: SqlColumn<T>) {
+   }
+   ```
+   
+   Should change to:
+
+   ```kotlin
+   import org.mybatis.dynamic.sql.SqlColumn
+
+   fun <T : Any> foo(column: SqlColumn<T>) {
+   }
+   ```
+
+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
+
 ## Release 1.5.2 - June 3, 2024
 
 This is a small maintenance release with the following changes:
diff --git a/README.md b/README.md
index 12dac83b3..3a536a47a 100644
--- a/README.md
+++ b/README.md
@@ -78,6 +78,10 @@ The library test cases provide several complete examples of using the library in
 | Kotlin   | Spring JDBC                              | Example using Kotlin utility classes for Spring JDBC Template                      | [../examples/kotlin/spring/canonical](src/test/kotlin/examples/kotlin/spring/canonical)                     |
 
 
-## Requirements
+## Requirements and Dependencies
 
-The library has no dependencies.  Java 8 or higher is required.
+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/checkstyle-override.xml b/checkstyle-override.xml
index 1e54f5578..17355d976 100644
--- a/checkstyle-override.xml
+++ b/checkstyle-override.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0"?>
 <!--
 
-       Copyright 2016-2024 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.
diff --git a/mvnw b/mvnw
index b21a698ee..668388825 100755
--- a/mvnw
+++ b/mvnw
@@ -19,7 +19,7 @@
 # ----------------------------------------------------------------------------
 
 # ----------------------------------------------------------------------------
-# Apache Maven Wrapper startup batch script, version 3.3.1
+# Apache Maven Wrapper startup batch script, version 3.3.2
 #
 # Required ENV vars:
 # ------------------
@@ -212,9 +212,9 @@ else
   log "Couldn't find $wrapperJarPath, downloading it ..."
 
   if [ -n "$MVNW_REPOURL" ]; then
-    wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.3.1/maven-wrapper-3.3.1.jar"
+    wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar"
   else
-    wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.1/maven-wrapper-3.3.1.jar"
+    wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar"
   fi
   while IFS="=" read -r key value; do
     # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
diff --git a/mvnw.cmd b/mvnw.cmd
index f93f29a8e..da4fe4dd9 100644
--- a/mvnw.cmd
+++ b/mvnw.cmd
@@ -18,7 +18,7 @@
 @REM ----------------------------------------------------------------------------
 
 @REM ----------------------------------------------------------------------------
-@REM Apache Maven Wrapper startup batch script, version 3.3.1
+@REM Apache Maven Wrapper startup batch script, version 3.3.2
 @REM
 @REM Required ENV vars:
 @REM JAVA_HOME - location of a JDK home dir
@@ -119,7 +119,7 @@ SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
 set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
 set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
 
-set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.1/maven-wrapper-3.3.1.jar"
+set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar"
 
 FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
     IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
@@ -133,7 +133,7 @@ if exist %WRAPPER_JAR% (
     )
 ) else (
     if not "%MVNW_REPOURL%" == "" (
-        SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.3.1/maven-wrapper-3.3.1.jar"
+        SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar"
     )
     if "%MVNW_VERBOSE%" == "true" (
         echo Couldn't find %WRAPPER_JAR%, downloading it ...
diff --git a/pom.xml b/pom.xml
index 284c3ff83..e1e1f613d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
 
-       Copyright 2016-2024 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.
@@ -21,12 +21,12 @@
   <parent>
     <groupId>org.mybatis</groupId>
     <artifactId>mybatis-parent</artifactId>
-    <version>44</version>
+    <version>48</version>
   </parent>
 
   <groupId>org.mybatis.dynamic-sql</groupId>
   <artifactId>mybatis-dynamic-sql</artifactId>
-  <version>1.5.2</version>
+  <version>2.0.0-SNAPSHOT</version>
 
   <name>MyBatis Dynamic SQL</name>
   <description>MyBatis framework for generating dynamic SQL</description>
@@ -36,7 +36,7 @@
   <scm>
     <connection>scm:git:ssh://git@github.com/mybatis/mybatis-dynamic-sql.git</connection>
     <developerConnection>scm:git:ssh://git@github.com/mybatis/mybatis-dynamic-sql.git</developerConnection>
-    <tag>mybatis-dynamic-sql-1.5.2</tag>
+    <tag>HEAD</tag>
     <url>https://github.com/mybatis/mybatis-dynamic-sql/</url>
   </scm>
   <issueManagement>
@@ -57,9 +57,11 @@
 
   <properties>
     <java.version>17</java.version>
-    <java.release.version>8</java.release.version>
-    <junit.jupiter.version>5.10.2</junit.jupiter.version>
-    <spring.batch.version>5.1.2</spring.batch.version>
+    <java.release.version>17</java.release.version>
+    <java.test.version>17</java.test.version>
+    <java.test.release.version>17</java.test.release.version>
+    <junit.jupiter.version>5.12.2</junit.jupiter.version>
+    <spring.batch.version>5.2.2</spring.batch.version>
 
     <checkstyle.config>checkstyle-override.xml</checkstyle.config>
 
@@ -67,21 +69,30 @@
 
     <module.name>org.mybatis.dynamic.sql</module.name>
 
-    <kotlin.version>2.0.0</kotlin.version>
-    <kotlin.compiler.jvmTarget>1.8</kotlin.compiler.jvmTarget>
+    <kotlin.version>2.1.20</kotlin.version>
+    <kotlin.compiler.jvmTarget>17</kotlin.compiler.jvmTarget>
+    <kotlin.compiler.languageVersion>2.0</kotlin.compiler.languageVersion>
+    <kotlin.compiler.apiVersion>2.0</kotlin.compiler.apiVersion>
 
     <sonar.sources>pom.xml,src/main/java,src/main/kotlin</sonar.sources>
     <sonar.tests>src/test/java,src/test/kotlin</sonar.tests>
+    <!-- setup sonar to run locally by default -->
+    <sonar.host.url>http://localhost:9000</sonar.host.url>
 
     <kotlin.code.style>official</kotlin.code.style>
-    <test.containers.version>1.19.8</test.containers.version>
+    <test.containers.version>1.20.6</test.containers.version>
     <osgi.export>org.mybatis.dynamic.sql.*;version=${project.version};-noimport:=true</osgi.export>
 
     <!-- Reproducible Builds -->
-    <project.build.outputTimestamp>1717449261</project.build.outputTimestamp>
+    <project.build.outputTimestamp>1717449335</project.build.outputTimestamp>
   </properties>
 
   <dependencies>
+    <dependency>
+      <groupId>org.jspecify</groupId>
+      <artifactId>jspecify</artifactId>
+      <version>1.0.0</version>
+    </dependency>
     <dependency>
       <groupId>org.jetbrains.kotlin</groupId>
       <artifactId>kotlin-stdlib-jdk8</artifactId>
@@ -92,14 +103,14 @@
     <dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-jdbc</artifactId>
-      <version>6.1.8</version>
+      <version>6.2.6</version>
       <scope>provided</scope>
       <optional>true</optional>
     </dependency>
     <dependency>
       <groupId>org.mybatis</groupId>
       <artifactId>mybatis</artifactId>
-      <version>3.5.16</version>
+      <version>3.5.19</version>
       <scope>provided</scope>
       <optional>true</optional>
     </dependency>
@@ -125,19 +136,19 @@
     <dependency>
       <groupId>org.assertj</groupId>
       <artifactId>assertj-core</artifactId>
-      <version>3.26.0</version>
+      <version>3.27.3</version>
       <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.mybatis</groupId>
       <artifactId>mybatis-spring</artifactId>
-      <version>3.0.3</version>
+      <version>3.0.4</version>
       <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.hsqldb</groupId>
       <artifactId>hsqldb</artifactId>
-      <version>2.7.3</version>
+      <version>2.7.4</version>
       <scope>test</scope>
     </dependency>
     <dependency>
@@ -161,33 +172,25 @@
     <dependency>
       <groupId>ch.qos.logback</groupId>
       <artifactId>logback-classic</artifactId>
-      <version>1.5.6</version>
+      <version>1.5.18</version>
       <scope>test</scope>
     </dependency>
-    <!-- Hamcrest is only here to make Infinitest happy. Not really used in the project. -->
-    <dependency>
-      <groupId>org.hamcrest</groupId>
-      <artifactId>hamcrest</artifactId>
-      <version>2.2</version>
-      <scope>test</scope>
-    </dependency>
-
     <dependency>
       <groupId>org.testcontainers</groupId>
-      <artifactId>postgresql</artifactId>
+      <artifactId>junit-jupiter</artifactId>
       <version>${test.containers.version}</version>
       <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.testcontainers</groupId>
-      <artifactId>junit-jupiter</artifactId>
+      <artifactId>postgresql</artifactId>
       <version>${test.containers.version}</version>
       <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.postgresql</groupId>
       <artifactId>postgresql</artifactId>
-      <version>42.7.3</version>
+      <version>42.7.5</version>
       <scope>test</scope>
     </dependency>
     <dependency>
@@ -199,7 +202,19 @@
     <dependency>
       <groupId>org.mariadb.jdbc</groupId>
       <artifactId>mariadb-java-client</artifactId>
-      <version>3.4.0</version>
+      <version>3.5.3</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.testcontainers</groupId>
+      <artifactId>mysql</artifactId>
+      <version>${test.containers.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.mysql</groupId>
+      <artifactId>mysql-connector-j</artifactId>
+      <version>9.2.0</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git a/src/main/java/org/mybatis/dynamic/sql/AbstractColumnComparisonCondition.java b/src/main/java/org/mybatis/dynamic/sql/AbstractColumnComparisonCondition.java
index 4e419d968..65e45aafe 100644
--- a/src/main/java/org/mybatis/dynamic/sql/AbstractColumnComparisonCondition.java
+++ b/src/main/java/org/mybatis/dynamic/sql/AbstractColumnComparisonCondition.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,7 +15,12 @@
  */
 package org.mybatis.dynamic.sql;
 
-public abstract class AbstractColumnComparisonCondition<T> implements VisitableCondition<T> {
+import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore;
+
+import org.mybatis.dynamic.sql.render.RenderingContext;
+import org.mybatis.dynamic.sql.util.FragmentAndParameters;
+
+public abstract class AbstractColumnComparisonCondition<T> implements RenderableCondition<T> {
 
     protected final BasicColumn rightColumn;
 
@@ -23,14 +28,10 @@ protected AbstractColumnComparisonCondition(BasicColumn rightColumn) {
         this.rightColumn = rightColumn;
     }
 
-    public BasicColumn rightColumn() {
-        return rightColumn;
-    }
+    public abstract String operator();
 
     @Override
-    public <R> R accept(ConditionVisitor<T, R> visitor) {
-        return visitor.visit(this);
+    public FragmentAndParameters renderCondition(RenderingContext renderingContext, BindableColumn<T> leftColumn) {
+        return rightColumn.render(renderingContext).mapFragment(f -> operator() + spaceBefore(f));
     }
-
-    public abstract String operator();
 }
diff --git a/src/main/java/org/mybatis/dynamic/sql/AbstractListValueCondition.java b/src/main/java/org/mybatis/dynamic/sql/AbstractListValueCondition.java
index 104859de8..e178c6bf3 100644
--- a/src/main/java/org/mybatis/dynamic/sql/AbstractListValueCondition.java
+++ b/src/main/java/org/mybatis/dynamic/sql/AbstractListValueCondition.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -23,7 +23,13 @@
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
-public abstract class AbstractListValueCondition<T> implements VisitableCondition<T> {
+import org.jspecify.annotations.NonNull;
+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<T> implements RenderableCondition<T> {
     protected final Collection<T> values;
 
     protected AbstractListValueCondition(Collection<T> values) {
@@ -39,19 +45,14 @@ public boolean isEmpty() {
         return values.isEmpty();
     }
 
-    @Override
-    public <R> R accept(ConditionVisitor<T, R> visitor) {
-        return visitor.visit(this);
-    }
-
     private <R> Collection<R> applyMapper(Function<? super T, ? extends R> mapper) {
         Objects.requireNonNull(mapper);
-        return values.stream().map(mapper).collect(Collectors.toList());
+        return values().map(mapper).collect(Collectors.toList());
     }
 
     private Collection<T> applyFilter(Predicate<? super T> predicate) {
         Objects.requireNonNull(predicate);
-        return values.stream().filter(predicate).collect(Collectors.toList());
+        return values().filter(predicate).toList();
     }
 
     protected <S extends AbstractListValueCondition<T>> S filterSupport(Predicate<? super T> predicate,
@@ -73,15 +74,71 @@ protected <R, S extends AbstractListValueCondition<R>> S mapSupport(Function<? s
         }
     }
 
+    public abstract String operator();
+
+    @Override
+    public FragmentAndParameters renderCondition(RenderingContext renderingContext, BindableColumn<T> 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$
+    }
+
+    private FragmentAndParameters toFragmentAndParameters(T value, RenderingContext renderingContext,
+                                                          BindableColumn<T> leftColumn) {
+        RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo(leftColumn);
+        return FragmentAndParameters.withFragment(parameterInfo.renderedPlaceHolder())
+                .withParameter(parameterInfo.parameterMapKey(), leftColumn.convertParameterType(value))
+                .build();
+    }
+
     /**
-     * If not empty, apply the predicate to each value in the list and return a new condition with the filtered values.
-     *     Else returns an empty condition (this).
+     * Conditions may implement Filterable to add optionality to rendering.
+     *
+     * <p>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.
      *
-     * @param predicate predicate applied to the values, if not empty
+     * <p>Implementations of Filterable may call
+     * {@link AbstractListValueCondition#filterSupport(Predicate, Function, AbstractListValueCondition, Supplier)} as
+     * a common implementation of the filtering algorithm.
      *
-     * @return a new condition with filtered values if renderable, otherwise an empty condition
+     * @param <T> the Java type related to the database column type
      */
-    public abstract AbstractListValueCondition<T> filter(Predicate<? super T> predicate);
+    public interface Filterable<T> {
+        /**
+         * 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<T> filter(Predicate<? super @NonNull T> predicate);
+    }
 
-    public abstract String operator();
+    /**
+     * Conditions may implement Mappable to alter condition values or types during rendering.
+     *
+     * <p>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.
+     *
+     * <p>Implementations of Mappable may call
+     * {@link AbstractListValueCondition#mapSupport(Function, Function, Supplier)} as
+     * a common implementation of the mapping algorithm.
+     *
+     * @param <T> the Java type related to the database column type
+     */
+    public interface Mappable<T> {
+        /**
+         * 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 <R> 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.
+         */
+        <R> AbstractListValueCondition<R> map(Function<? super @NonNull T, ? extends R> mapper);
+    }
 }
diff --git a/src/main/java/org/mybatis/dynamic/sql/AbstractNoValueCondition.java b/src/main/java/org/mybatis/dynamic/sql/AbstractNoValueCondition.java
index d6b1c384b..71daa7763 100644
--- a/src/main/java/org/mybatis/dynamic/sql/AbstractNoValueCondition.java
+++ b/src/main/java/org/mybatis/dynamic/sql/AbstractNoValueCondition.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,12 +18,10 @@
 import java.util.function.BooleanSupplier;
 import java.util.function.Supplier;
 
-public abstract class AbstractNoValueCondition<T> implements VisitableCondition<T> {
+import org.mybatis.dynamic.sql.render.RenderingContext;
+import org.mybatis.dynamic.sql.util.FragmentAndParameters;
 
-    @Override
-    public <R> R accept(ConditionVisitor<T, R> visitor) {
-        return visitor.visit(this);
-    }
+public abstract class AbstractNoValueCondition<T> implements RenderableCondition<T> {
 
     protected <S extends AbstractNoValueCondition<?>> S filterSupport(BooleanSupplier booleanSupplier,
             Supplier<S> emptySupplier, S self) {
@@ -35,4 +33,36 @@ protected <S extends AbstractNoValueCondition<?>> S filterSupport(BooleanSupplie
     }
 
     public abstract String operator();
+
+    @Override
+    public FragmentAndParameters renderCondition(RenderingContext renderingContext, BindableColumn<T> leftColumn) {
+        return FragmentAndParameters.fromFragment(operator());
+    }
+
+    /**
+     * Conditions may implement Filterable to add optionality to rendering.
+     *
+     * <p>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.
+     *
+     * <p>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 <S>
+         *            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.
+         */
+        <S> AbstractNoValueCondition<S> filter(BooleanSupplier booleanSupplier);
+    }
 }
diff --git a/src/main/java/org/mybatis/dynamic/sql/AbstractSingleValueCondition.java b/src/main/java/org/mybatis/dynamic/sql/AbstractSingleValueCondition.java
index 1e70adb31..eb56eef39 100644
--- a/src/main/java/org/mybatis/dynamic/sql/AbstractSingleValueCondition.java
+++ b/src/main/java/org/mybatis/dynamic/sql/AbstractSingleValueCondition.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,11 +15,18 @@
  */
 package org.mybatis.dynamic.sql;
 
+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<T> implements VisitableCondition<T> {
+import org.jspecify.annotations.NonNull;
+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<T> implements RenderableCondition<T> {
     protected final T value;
 
     protected AbstractSingleValueCondition(T value) {
@@ -30,11 +37,6 @@ public T value() {
         return value;
     }
 
-    @Override
-    public <R> R accept(ConditionVisitor<T, R> visitor) {
-        return visitor.visit(this);
-    }
-
     protected <S extends AbstractSingleValueCondition<T>> S filterSupport(Predicate<? super T> predicate,
             Supplier<S> emptySupplier, S self) {
         if (isEmpty()) {
@@ -53,15 +55,65 @@ protected <R, S extends AbstractSingleValueCondition<R>> S mapSupport(Function<?
         }
     }
 
+    public abstract String operator();
+
+    @Override
+    public FragmentAndParameters renderCondition(RenderingContext renderingContext, BindableColumn<T> leftColumn) {
+        RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo(leftColumn);
+        String finalFragment = operator() + spaceBefore(parameterInfo.renderedPlaceHolder());
+
+        return FragmentAndParameters.withFragment(finalFragment)
+                .withParameter(parameterInfo.parameterMapKey(), leftColumn.convertParameterType(value()))
+                .build();
+    }
+
     /**
-     * If renderable and the value matches the predicate, returns this condition. Else returns a condition
-     *     that will not render.
+     * Conditions may implement Filterable to add optionality to rendering.
      *
-     * @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.
+     * <p>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.
+     *
+     * <p>Implementations of Filterable may call
+     * {@link AbstractSingleValueCondition#filterSupport(Predicate, Supplier, AbstractSingleValueCondition)} as
+     * a common implementation of the filtering algorithm.
+     *
+     * @param <T> the Java type related to the database column type
      */
-    public abstract AbstractSingleValueCondition<T> filter(Predicate<? super T> predicate);
+    public interface Filterable<T> {
+        /**
+         * 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<T> filter(Predicate<? super @NonNull T> predicate);
+    }
 
-    public abstract String operator();
+    /**
+     * Conditions may implement Mappable to alter condition values or types during rendering.
+     *
+     * <p>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.
+     *
+     * <p>Implementations of Mappable may call
+     * {@link AbstractSingleValueCondition#mapSupport(Function, Function, Supplier)} as
+     * a common implementation of the mapping algorithm.
+     *
+     * @param <T> the Java type related to the database column type
+     */
+    public interface Mappable<T> {
+        /**
+         * 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 <R> 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.
+         */
+        <R> AbstractSingleValueCondition<R> map(Function<? super @NonNull T, ? extends R> mapper);
+    }
 }
diff --git a/src/main/java/org/mybatis/dynamic/sql/AbstractSubselectCondition.java b/src/main/java/org/mybatis/dynamic/sql/AbstractSubselectCondition.java
index f17e29066..dcfbd4b3c 100644
--- a/src/main/java/org/mybatis/dynamic/sql/AbstractSubselectCondition.java
+++ b/src/main/java/org/mybatis/dynamic/sql/AbstractSubselectCondition.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -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<T> implements VisitableCondition<T> {
+public abstract class AbstractSubselectCondition<T> implements RenderableCondition<T> {
     private final SelectModel selectModel;
 
     protected AbstractSubselectCondition(Buildable<SelectModel> selectModelBuilder) {
         this.selectModel = selectModelBuilder.build();
     }
 
-    public SelectModel selectModel() {
-        return selectModel;
-    }
+    public abstract String operator();
 
     @Override
-    public <R> R accept(ConditionVisitor<T, R> visitor) {
-        return visitor.visit(this);
+    public FragmentAndParameters renderCondition(RenderingContext renderingContext, BindableColumn<T> leftColumn) {
+        return SubQueryRenderer.withSelectModel(selectModel)
+                .withRenderingContext(renderingContext)
+                .withPrefix(operator() + " (") //$NON-NLS-1$
+                .withSuffix(")") //$NON-NLS-1$
+                .build()
+                .render();
     }
-
-    public abstract String operator();
 }
diff --git a/src/main/java/org/mybatis/dynamic/sql/AbstractTwoValueCondition.java b/src/main/java/org/mybatis/dynamic/sql/AbstractTwoValueCondition.java
index ee8b3c577..d409ffbb8 100644
--- a/src/main/java/org/mybatis/dynamic/sql/AbstractTwoValueCondition.java
+++ b/src/main/java/org/mybatis/dynamic/sql/AbstractTwoValueCondition.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,13 +15,20 @@
  */
 package org.mybatis.dynamic.sql;
 
+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<T> implements VisitableCondition<T> {
+import org.jspecify.annotations.NonNull;
+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<T> implements RenderableCondition<T> {
     protected final T value1;
     protected final T value2;
 
@@ -38,11 +45,6 @@ public T value2() {
         return value2;
     }
 
-    @Override
-    public <R> R accept(ConditionVisitor<T, R> visitor) {
-        return visitor.visit(this);
-    }
-
     protected <S extends AbstractTwoValueCondition<T>> S filterSupport(BiPredicate<? super T, ? super T> predicate,
             Supplier<S> emptySupplier, S self) {
         if (isEmpty()) {
@@ -66,28 +68,98 @@ protected <R, S extends AbstractTwoValueCondition<R>> S mapSupport(Function<? su
         }
     }
 
+    public abstract String operator1();
+
+    public abstract String operator2();
+
+    @Override
+    public FragmentAndParameters renderCondition(RenderingContext renderingContext, BindableColumn<T> 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();
+    }
+
     /**
-     * If renderable and the values match the predicate, returns this condition. Else returns a condition
-     *     that will not render.
+     * Conditions may implement Filterable to add optionality to rendering.
+     *
+     * <p>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.
      *
-     * @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.
+     * <p>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 <T> the Java type related to the database column type
      */
-    public abstract AbstractTwoValueCondition<T> filter(BiPredicate<? super T, ? super T> predicate);
+    public interface Filterable<T> {
+        /**
+         * 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<T> filter(BiPredicate<? super @NonNull T, ? super @NonNull T> 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<T> filter(Predicate<? super @NonNull T> 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.
+     * Conditions may implement Mappable to alter condition values or types during rendering.
      *
-     * @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.
+     * <p>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.
+     *
+     * <p>Implementations of Mappable may call
+     * {@link AbstractTwoValueCondition#mapSupport(Function, Function, BiFunction, Supplier)} as
+     * a common implementation of the mapping algorithm.
+     *
+     * @param <T> the Java type related to the database column type
      */
-    public abstract AbstractTwoValueCondition<T> filter(Predicate<? super T> predicate);
-
-    public abstract String operator1();
+    public interface Mappable<T> {
+        /**
+         * 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 <R> 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.
+         */
+        <R> AbstractTwoValueCondition<R> map(Function<? super @NonNull T, ? extends R> mapper1,
+                                             Function<? super @NonNull T, ? extends R> mapper2);
 
-    public abstract String operator2();
+        /**
+         * 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 <R> 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.
+         */
+        <R> AbstractTwoValueCondition<R> map(Function<? super @NonNull T, ? extends R> mapper);
+    }
 }
diff --git a/src/main/java/org/mybatis/dynamic/sql/AliasableSqlTable.java b/src/main/java/org/mybatis/dynamic/sql/AliasableSqlTable.java
index 4e6c32386..915b6a561 100644
--- a/src/main/java/org/mybatis/dynamic/sql/AliasableSqlTable.java
+++ b/src/main/java/org/mybatis/dynamic/sql/AliasableSqlTable.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,9 +19,11 @@
 import java.util.Optional;
 import java.util.function.Supplier;
 
+import org.jspecify.annotations.Nullable;
+
 public abstract class AliasableSqlTable<T extends AliasableSqlTable<T>> extends SqlTable {
 
-    private String tableAlias;
+    private @Nullable String tableAlias;
     private final Supplier<T> constructor;
 
     protected AliasableSqlTable(String tableName, Supplier<T> constructor) {
@@ -32,7 +34,7 @@ protected AliasableSqlTable(String tableName, Supplier<T> constructor) {
     public T withAlias(String alias) {
         T newTable = constructor.get();
         ((AliasableSqlTable<T>) newTable).tableAlias = alias;
-        newTable.nameSupplier = nameSupplier;
+        newTable.tableName = tableName;
         return newTable;
     }
 
@@ -48,7 +50,7 @@ public T withName(String name) {
         Objects.requireNonNull(name);
         T newTable = constructor.get();
         ((AliasableSqlTable<T>) newTable).tableAlias = tableAlias;
-        newTable.nameSupplier = () -> name;
+        newTable.tableName = name;
         return newTable;
     }
 
diff --git a/src/main/java/org/mybatis/dynamic/sql/AndOrCriteriaGroup.java b/src/main/java/org/mybatis/dynamic/sql/AndOrCriteriaGroup.java
index a85861904..ff630a036 100644
--- a/src/main/java/org/mybatis/dynamic/sql/AndOrCriteriaGroup.java
+++ b/src/main/java/org/mybatis/dynamic/sql/AndOrCriteriaGroup.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -21,6 +21,8 @@
 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
@@ -32,7 +34,7 @@
  */
 public class AndOrCriteriaGroup {
     private final String connector;
-    private final SqlCriterion initialCriterion;
+    private final @Nullable SqlCriterion initialCriterion;
     private final List<AndOrCriteriaGroup> subCriteria;
 
     private AndOrCriteriaGroup(Builder builder) {
@@ -54,8 +56,8 @@ public List<AndOrCriteriaGroup> subCriteria() {
     }
 
     public static class Builder {
-        private String connector;
-        private SqlCriterion initialCriterion;
+        private @Nullable String connector;
+        private @Nullable SqlCriterion initialCriterion;
         private final List<AndOrCriteriaGroup> subCriteria = new ArrayList<>();
 
         public Builder withConnector(String connector) {
@@ -63,7 +65,7 @@ public Builder withConnector(String connector) {
             return this;
         }
 
-        public Builder withInitialCriterion(SqlCriterion initialCriterion) {
+        public Builder withInitialCriterion(@Nullable SqlCriterion initialCriterion) {
             this.initialCriterion = initialCriterion;
             return this;
         }
diff --git a/src/main/java/org/mybatis/dynamic/sql/BasicColumn.java b/src/main/java/org/mybatis/dynamic/sql/BasicColumn.java
index aaee827b1..2573b4529 100644
--- a/src/main/java/org/mybatis/dynamic/sql/BasicColumn.java
+++ b/src/main/java/org/mybatis/dynamic/sql/BasicColumn.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,13 +15,12 @@
  */
 package org.mybatis.dynamic.sql;
 
+import java.sql.JDBCType;
 import java.util.Optional;
 
-import org.mybatis.dynamic.sql.exception.DynamicSqlException;
 import org.mybatis.dynamic.sql.render.RenderingContext;
-import org.mybatis.dynamic.sql.render.TableAliasCalculator;
+import org.mybatis.dynamic.sql.render.RenderingStrategy;
 import org.mybatis.dynamic.sql.util.FragmentAndParameters;
-import org.mybatis.dynamic.sql.util.Messages;
 
 /**
  * Describes attributes of columns that are necessary for rendering if the column is not expected to
@@ -59,24 +58,18 @@ public interface BasicColumn {
      * @return a rendered SQL fragment and, optionally, parameters associated with the fragment
      * @since 1.5.1
      */
-    default FragmentAndParameters render(RenderingContext renderingContext) {
-        // the default implementation ensures compatibility with prior releases. When the
-        // deprecated renderWithTableAlias method is removed, this function can become purely abstract.
-        // Also remove the method tableAliasCalculator() from RenderingContext.
-        return FragmentAndParameters.fromFragment(renderWithTableAlias(renderingContext.tableAliasCalculator()));
+    FragmentAndParameters render(RenderingContext renderingContext);
+
+    default Optional<JDBCType> jdbcType() {
+        return Optional.empty();
     }
 
-    /**
-     * 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
-     * @deprecated Please replace this method by overriding the more general "render" method
-     */
-    @Deprecated
-    default String renderWithTableAlias(TableAliasCalculator tableAliasCalculator) {
-        throw new DynamicSqlException(Messages.getString("ERROR.36"));  //$NON-NLS-1$
+    default Optional<String> typeHandler() {
+        return Optional.empty();
+    }
+
+    default Optional<RenderingStrategy> renderingStrategy() {
+        return Optional.empty();
     }
 
     /**
diff --git a/src/main/java/org/mybatis/dynamic/sql/BindableColumn.java b/src/main/java/org/mybatis/dynamic/sql/BindableColumn.java
index 274b52e25..0fd90b7c8 100644
--- a/src/main/java/org/mybatis/dynamic/sql/BindableColumn.java
+++ b/src/main/java/org/mybatis/dynamic/sql/BindableColumn.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,14 +15,13 @@
  */
 package org.mybatis.dynamic.sql;
 
-import java.sql.JDBCType;
 import java.util.Optional;
 
-import org.mybatis.dynamic.sql.render.RenderingStrategy;
+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
  *
@@ -37,19 +36,7 @@ public interface BindableColumn<T> extends BasicColumn {
     @Override
     BindableColumn<T> as(String alias);
 
-    default Optional<JDBCType> jdbcType() {
-        return Optional.empty();
-    }
-
-    default Optional<String> typeHandler() {
-        return Optional.empty();
-    }
-
-    default Optional<RenderingStrategy> renderingStrategy() {
-        return Optional.empty();
-    }
-
-    default Object convertParameterType(T value) {
+    default @Nullable Object convertParameterType(T value) {
         return value;
     }
 
diff --git a/src/main/java/org/mybatis/dynamic/sql/BoundValue.java b/src/main/java/org/mybatis/dynamic/sql/BoundValue.java
index 7ac0bd26a..5151a5dad 100644
--- a/src/main/java/org/mybatis/dynamic/sql/BoundValue.java
+++ b/src/main/java/org/mybatis/dynamic/sql/BoundValue.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/main/java/org/mybatis/dynamic/sql/ColumnAndConditionCriterion.java b/src/main/java/org/mybatis/dynamic/sql/ColumnAndConditionCriterion.java
index 9c0f47151..053c18f64 100644
--- a/src/main/java/org/mybatis/dynamic/sql/ColumnAndConditionCriterion.java
+++ b/src/main/java/org/mybatis/dynamic/sql/ColumnAndConditionCriterion.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,9 +17,11 @@
 
 import java.util.Objects;
 
+import org.jspecify.annotations.Nullable;
+
 public class ColumnAndConditionCriterion<T> extends SqlCriterion {
     private final BindableColumn<T> column;
-    private final VisitableCondition<T> condition;
+    private final RenderableCondition<T> condition;
 
     private ColumnAndConditionCriterion(Builder<T> builder) {
         super(builder);
@@ -31,7 +33,7 @@ public BindableColumn<T> column() {
         return column;
     }
 
-    public VisitableCondition<T> condition() {
+    public RenderableCondition<T> condition() {
         return condition;
     }
 
@@ -45,15 +47,15 @@ public static <T> Builder<T> withColumn(BindableColumn<T> column) {
     }
 
     public static class Builder<T> extends AbstractBuilder<Builder<T>> {
-        private BindableColumn<T> column;
-        private VisitableCondition<T> condition;
+        private @Nullable BindableColumn<T> column;
+        private @Nullable RenderableCondition<T> condition;
 
         public Builder<T> withColumn(BindableColumn<T> column) {
             this.column = column;
             return this;
         }
 
-        public Builder<T> withCondition(VisitableCondition<T> condition) {
+        public Builder<T> withCondition(RenderableCondition<T> condition) {
             this.condition = condition;
             return this;
         }
diff --git a/src/main/java/org/mybatis/dynamic/sql/ConditionVisitor.java b/src/main/java/org/mybatis/dynamic/sql/ConditionVisitor.java
deleted file mode 100644
index c8e95a9bd..000000000
--- a/src/main/java/org/mybatis/dynamic/sql/ConditionVisitor.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- *    Copyright 2016-2024 the original author or authors.
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *       https://www.apache.org/licenses/LICENSE-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 ConditionVisitor<T, R> {
-    R visit(AbstractListValueCondition<T> condition);
-
-    R visit(AbstractNoValueCondition<T> condition);
-
-    R visit(AbstractSingleValueCondition<T> condition);
-
-    R visit(AbstractTwoValueCondition<T> condition);
-
-    R visit(AbstractSubselectCondition<T> condition);
-
-    R visit(AbstractColumnComparisonCondition<T> condition);
-}
diff --git a/src/main/java/org/mybatis/dynamic/sql/Constant.java b/src/main/java/org/mybatis/dynamic/sql/Constant.java
index d424f6391..90547765f 100644
--- a/src/main/java/org/mybatis/dynamic/sql/Constant.java
+++ b/src/main/java/org/mybatis/dynamic/sql/Constant.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,19 +18,20 @@
 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;
 
 public class Constant<T> implements BindableColumn<T> {
 
-    private final String alias;
+    private final @Nullable String alias;
     private final String value;
 
     private Constant(String value) {
         this(value, null);
     }
 
-    private Constant(String value, String alias) {
+    private Constant(String value, @Nullable String alias) {
         this.value = Objects.requireNonNull(value);
         this.alias = alias;
     }
diff --git a/src/main/java/org/mybatis/dynamic/sql/CriteriaGroup.java b/src/main/java/org/mybatis/dynamic/sql/CriteriaGroup.java
index 931b55722..476cb6b3f 100644
--- a/src/main/java/org/mybatis/dynamic/sql/CriteriaGroup.java
+++ b/src/main/java/org/mybatis/dynamic/sql/CriteriaGroup.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,6 +17,8 @@
 
 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
@@ -27,7 +29,7 @@
  * @since 1.4.0
  */
 public class CriteriaGroup extends SqlCriterion {
-    private final SqlCriterion initialCriterion;
+    private final @Nullable SqlCriterion initialCriterion;
 
     protected CriteriaGroup(AbstractGroupBuilder<?> builder) {
         super(builder);
@@ -44,9 +46,9 @@ public <R> R accept(SqlCriterionVisitor<R> visitor) {
     }
 
     public abstract static class AbstractGroupBuilder<T extends AbstractGroupBuilder<T>> extends AbstractBuilder<T> {
-        private SqlCriterion initialCriterion;
+        private @Nullable SqlCriterion initialCriterion;
 
-        public T withInitialCriterion(SqlCriterion initialCriterion) {
+        public T withInitialCriterion(@Nullable SqlCriterion initialCriterion) {
             this.initialCriterion = initialCriterion;
             return getThis();
         }
diff --git a/src/main/java/org/mybatis/dynamic/sql/DerivedColumn.java b/src/main/java/org/mybatis/dynamic/sql/DerivedColumn.java
index 73ca62526..df19b8d6b 100644
--- a/src/main/java/org/mybatis/dynamic/sql/DerivedColumn.java
+++ b/src/main/java/org/mybatis/dynamic/sql/DerivedColumn.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,6 +19,7 @@
 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;
 
@@ -34,10 +35,10 @@
  */
 public class DerivedColumn<T> implements BindableColumn<T> {
     private final String name;
-    private final String tableQualifier;
-    private final String columnAlias;
-    private final JDBCType jdbcType;
-    private final String typeHandler;
+    private final @Nullable String tableQualifier;
+    private final @Nullable String columnAlias;
+    private final @Nullable JDBCType jdbcType;
+    private final @Nullable String typeHandler;
 
     protected DerivedColumn(Builder<T> builder) {
         this.name = Objects.requireNonNull(builder.name);
@@ -93,18 +94,18 @@ public static <T> DerivedColumn<T> of(String name, String tableQualifier) {
     }
 
     public static class Builder<T> {
-        private String name;
-        private String tableQualifier;
-        private String columnAlias;
-        private JDBCType jdbcType;
-        private String typeHandler;
+        private @Nullable String name;
+        private @Nullable String tableQualifier;
+        private @Nullable String columnAlias;
+        private @Nullable JDBCType jdbcType;
+        private @Nullable String typeHandler;
 
         public Builder<T> withName(String name) {
             this.name = name;
             return this;
         }
 
-        public Builder<T> withTableQualifier(String tableQualifier) {
+        public Builder<T> withTableQualifier(@Nullable String tableQualifier) {
             this.tableQualifier = tableQualifier;
             return this;
         }
@@ -114,12 +115,12 @@ public Builder<T> withColumnAlias(String columnAlias) {
             return this;
         }
 
-        public Builder<T> withJdbcType(JDBCType jdbcType) {
+        public Builder<T> withJdbcType(@Nullable JDBCType jdbcType) {
             this.jdbcType = jdbcType;
             return this;
         }
 
-        public Builder<T> withTypeHandler(String typeHandler) {
+        public Builder<T> withTypeHandler(@Nullable String typeHandler) {
             this.typeHandler = typeHandler;
             return this;
         }
diff --git a/src/main/java/org/mybatis/dynamic/sql/ExistsCriterion.java b/src/main/java/org/mybatis/dynamic/sql/ExistsCriterion.java
index 711b85ffd..17cecdaa6 100644
--- a/src/main/java/org/mybatis/dynamic/sql/ExistsCriterion.java
+++ b/src/main/java/org/mybatis/dynamic/sql/ExistsCriterion.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,6 +17,8 @@
 
 import java.util.Objects;
 
+import org.jspecify.annotations.Nullable;
+
 public class ExistsCriterion extends SqlCriterion {
     private final ExistsPredicate existsPredicate;
 
@@ -35,7 +37,7 @@ public <R> R accept(SqlCriterionVisitor<R> visitor) {
     }
 
     public static class Builder extends AbstractBuilder<Builder> {
-        private ExistsPredicate existsPredicate;
+        private @Nullable ExistsPredicate existsPredicate;
 
         public Builder withExistsPredicate(ExistsPredicate existsPredicate) {
             this.existsPredicate = existsPredicate;
diff --git a/src/main/java/org/mybatis/dynamic/sql/ExistsPredicate.java b/src/main/java/org/mybatis/dynamic/sql/ExistsPredicate.java
index 8d68a3ac9..c22a84aa2 100644
--- a/src/main/java/org/mybatis/dynamic/sql/ExistsPredicate.java
+++ b/src/main/java/org/mybatis/dynamic/sql/ExistsPredicate.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,7 +17,6 @@
 
 import java.util.Objects;
 
-import org.jetbrains.annotations.NotNull;
 import org.mybatis.dynamic.sql.select.SelectModel;
 import org.mybatis.dynamic.sql.util.Buildable;
 
@@ -38,12 +37,10 @@ public Buildable<SelectModel> selectModelBuilder() {
         return selectModelBuilder;
     }
 
-    @NotNull
     public static ExistsPredicate exists(Buildable<SelectModel> selectModelBuilder) {
         return new ExistsPredicate("exists", selectModelBuilder); //$NON-NLS-1$
     }
 
-    @NotNull
     public static ExistsPredicate notExists(Buildable<SelectModel> 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
index cd9f5e980..f0a0010f8 100644
--- a/src/main/java/org/mybatis/dynamic/sql/NotCriterion.java
+++ b/src/main/java/org/mybatis/dynamic/sql/NotCriterion.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/main/java/org/mybatis/dynamic/sql/ParameterTypeConverter.java b/src/main/java/org/mybatis/dynamic/sql/ParameterTypeConverter.java
index 7df7572b1..4f4856e19 100644
--- a/src/main/java/org/mybatis/dynamic/sql/ParameterTypeConverter.java
+++ b/src/main/java/org/mybatis/dynamic/sql/ParameterTypeConverter.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,6 +15,8 @@
  */
 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
@@ -50,5 +52,13 @@
  */
 @FunctionalInterface
 public interface ParameterTypeConverter<S, T> {
-    T convert(S source);
+    /**
+     * Convert the value from one value to another.
+     *
+     * <p>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<T> {
+    /**
+     * Render a condition - typically a condition in a WHERE clause.
+     *
+     * <p>A rendered condition includes an SQL fragment, and any associated parameters. For example,
+     * the <code>isEqual</code> condition should be rendered as "= ?" where "?" is a properly formatted
+     * parameter marker (the parameter marker can be computed from the <code>RenderingContext</code>).
+     * 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<T> leftColumn);
+
+    /**
+     * Render the column in a column and condition phrase - typically in a WHERE clause.
+     *
+     * <p>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<T> 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 a17557017..13cfdee55 100644
--- a/src/main/java/org/mybatis/dynamic/sql/SortSpecification.java
+++ b/src/main/java/org/mybatis/dynamic/sql/SortSpecification.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,6 +15,9 @@
  */
 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.
  *
@@ -30,17 +33,12 @@ public interface SortSpecification {
     SortSpecification descending();
 
     /**
-     * Return the phrase that should be written into a rendered order by clause. This should
-     * NOT include the "DESC" word for descending sort specifications.
-     *
-     * @return the order by phrase
-     */
-    String orderByName();
-
-    /**
-     * Return true if the sort order is descending.
+     * Return a fragment rendered for use in an ORDER BY clause. The fragment should include "DESC" if a
+     * descending order is desired.
      *
-     * @return true if the SortSpecification should render as descending
+     * @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 6516696f6..9790633b0 100644
--- a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java
+++ b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -21,6 +21,8 @@
 import java.util.Objects;
 import java.util.function.Supplier;
 
+import org.jspecify.annotations.NonNull;
+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;
@@ -56,23 +58,23 @@
 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.EqualToValue;
-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;
 import org.mybatis.dynamic.sql.where.WhereDSL;
 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;
@@ -83,13 +85,19 @@
 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;
@@ -98,6 +106,8 @@
 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;
 
@@ -253,7 +263,7 @@ static WhereDSL.StandaloneWhereFinisher where() {
         return new WhereDSL().where();
     }
 
-    static <T> WhereDSL.StandaloneWhereFinisher where(BindableColumn<T> column, VisitableCondition<T> condition,
+    static <T> WhereDSL.StandaloneWhereFinisher where(BindableColumn<T> column, RenderableCondition<T> condition,
                                                       AndOrCriteriaGroup... subCriteria) {
         return new WhereDSL().where(column, condition, subCriteria);
     }
@@ -266,7 +276,7 @@ static WhereDSL.StandaloneWhereFinisher where(ExistsPredicate existsPredicate, A
         return new WhereDSL().where(existsPredicate, subCriteria);
     }
 
-    static <T> HavingDSL.StandaloneHavingFinisher having(BindableColumn<T> column, VisitableCondition<T> condition,
+    static <T> HavingDSL.StandaloneHavingFinisher having(BindableColumn<T> column, RenderableCondition<T> condition,
                                               AndOrCriteriaGroup... subCriteria) {
         return new HavingDSL().having(column, condition, subCriteria);
     }
@@ -276,12 +286,12 @@ static HavingDSL.StandaloneHavingFinisher having(SqlCriterion initialCriterion,
     }
 
     // where condition connectors
-    static <T> CriteriaGroup group(BindableColumn<T> column, VisitableCondition<T> condition,
+    static <T> CriteriaGroup group(BindableColumn<T> column, RenderableCondition<T> condition,
                                    AndOrCriteriaGroup... subCriteria) {
         return group(column, condition, Arrays.asList(subCriteria));
     }
 
-    static <T> CriteriaGroup group(BindableColumn<T> column, VisitableCondition<T> condition,
+    static <T> CriteriaGroup group(BindableColumn<T> column, RenderableCondition<T> condition,
                                    List<AndOrCriteriaGroup> subCriteria) {
         return new CriteriaGroup.Builder()
                 .withInitialCriterion(new ColumnAndConditionCriterion.Builder<T>().withColumn(column)
@@ -319,12 +329,12 @@ static CriteriaGroup group(List<AndOrCriteriaGroup> subCriteria) {
                 .build();
     }
 
-    static <T> NotCriterion not(BindableColumn<T> column, VisitableCondition<T> condition,
+    static <T> NotCriterion not(BindableColumn<T> column, RenderableCondition<T> condition,
                                 AndOrCriteriaGroup... subCriteria) {
         return not(column, condition, Arrays.asList(subCriteria));
     }
 
-    static <T> NotCriterion not(BindableColumn<T> column, VisitableCondition<T> condition,
+    static <T> NotCriterion not(BindableColumn<T> column, RenderableCondition<T> condition,
                                 List<AndOrCriteriaGroup> subCriteria) {
         return new NotCriterion.Builder()
                 .withInitialCriterion(new ColumnAndConditionCriterion.Builder<T>().withColumn(column)
@@ -362,7 +372,7 @@ static NotCriterion not(List<AndOrCriteriaGroup> subCriteria) {
                 .build();
     }
 
-    static <T> AndOrCriteriaGroup or(BindableColumn<T> column, VisitableCondition<T> condition,
+    static <T> AndOrCriteriaGroup or(BindableColumn<T> column, RenderableCondition<T> condition,
                                      AndOrCriteriaGroup... subCriteria) {
         return new AndOrCriteriaGroup.Builder()
                 .withInitialCriterion(ColumnAndConditionCriterion.withColumn(column)
@@ -397,7 +407,7 @@ static AndOrCriteriaGroup or(List<AndOrCriteriaGroup> subCriteria) {
                 .build();
     }
 
-    static <T> AndOrCriteriaGroup and(BindableColumn<T> column, VisitableCondition<T> condition,
+    static <T> AndOrCriteriaGroup and(BindableColumn<T> column, RenderableCondition<T> condition,
                                       AndOrCriteriaGroup... subCriteria) {
         return new AndOrCriteriaGroup.Builder()
                 .withInitialCriterion(ColumnAndConditionCriterion.withColumn(column)
@@ -433,20 +443,36 @@ static AndOrCriteriaGroup and(List<AndOrCriteriaGroup> subCriteria) {
     }
 
     // join support
-    static <T> JoinCriterion<T> and(BindableColumn<T> joinColumn, JoinCondition<T> joinCondition) {
-        return new JoinCriterion.Builder<T>()
-                .withConnector("and") //$NON-NLS-1$
-                .withJoinColumn(joinColumn)
-                .withJoinCondition(joinCondition)
+    static <T> ColumnAndConditionCriterion<T> on(BindableColumn<T> joinColumn, RenderableCondition<T> joinCondition) {
+        return ColumnAndConditionCriterion.withColumn(joinColumn)
+                .withCondition(joinCondition)
                 .build();
     }
 
-    static <T> JoinCriterion<T> on(BindableColumn<T> joinColumn, JoinCondition<T> joinCondition) {
-        return new JoinCriterion.Builder<T>()
-                .withConnector("on") //$NON-NLS-1$
-                .withJoinColumn(joinColumn)
-                .withJoinCondition(joinCondition)
-                .build();
+    /**
+     * Starting in version 2.0.0, this function is a synonym for {@link SqlBuilder#isEqualTo(BasicColumn)}.
+     *
+     * @param column the column
+     * @param <T> 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 <T> IsEqualToColumn<T> equalTo(BindableColumn<T> 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 <T> 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 <T> IsEqualTo<T> equalTo(T value) {
+        return isEqualTo(value);
     }
 
     // case expressions
@@ -460,14 +486,6 @@ static SearchedCaseDSL case_() {
         return SearchedCaseDSL.searchedCase();
     }
 
-    static <T> EqualTo<T> equalTo(BindableColumn<T> column) {
-        return new EqualTo<>(column);
-    }
-
-    static <T> EqualToValue<T> equalTo(T value) {
-        return new EqualToValue<>(value);
-    }
-
     // aggregate support
     static CountAll count() {
         return new CountAll();
@@ -497,7 +515,11 @@ static <T> Sum<T> sum(BindableColumn<T> column) {
         return Sum.of(column);
     }
 
-    static <T> Sum<T> sum(BindableColumn<T> column, VisitableCondition<T> condition) {
+    static Sum<Object> sum(BasicColumn column) {
+        return Sum.of(column);
+    }
+
+    static <T> Sum<T> sum(BindableColumn<T> column, RenderableCondition<T> condition) {
         return Sum.of(column, condition);
     }
 
@@ -625,11 +647,11 @@ static <T> IsEqualToColumn<T> isEqualTo(BasicColumn column) {
         return IsEqualToColumn.of(column);
     }
 
-    static <T> IsEqualTo<T> isEqualToWhenPresent(T value) {
-        return IsEqualTo.of(value).filter(Objects::nonNull);
+    static <T> IsEqualToWhenPresent<T> isEqualToWhenPresent(@Nullable T value) {
+        return IsEqualToWhenPresent.of(value);
     }
 
-    static <T> IsEqualTo<T> isEqualToWhenPresent(Supplier<T> valueSupplier) {
+    static <T> IsEqualToWhenPresent<T> isEqualToWhenPresent(Supplier<@Nullable T> valueSupplier) {
         return isEqualToWhenPresent(valueSupplier.get());
     }
 
@@ -649,11 +671,11 @@ static <T> IsNotEqualToColumn<T> isNotEqualTo(BasicColumn column) {
         return IsNotEqualToColumn.of(column);
     }
 
-    static <T> IsNotEqualTo<T> isNotEqualToWhenPresent(T value) {
-        return IsNotEqualTo.of(value).filter(Objects::nonNull);
+    static <T> IsNotEqualToWhenPresent<T> isNotEqualToWhenPresent(@Nullable T value) {
+        return IsNotEqualToWhenPresent.of(value);
     }
 
-    static <T> IsNotEqualTo<T> isNotEqualToWhenPresent(Supplier<T> valueSupplier) {
+    static <T> IsNotEqualToWhenPresent<T> isNotEqualToWhenPresent(Supplier<@Nullable T> valueSupplier) {
         return isNotEqualToWhenPresent(valueSupplier.get());
     }
 
@@ -673,11 +695,11 @@ static <T> IsGreaterThanColumn<T> isGreaterThan(BasicColumn column) {
         return IsGreaterThanColumn.of(column);
     }
 
-    static <T> IsGreaterThan<T> isGreaterThanWhenPresent(T value) {
-        return IsGreaterThan.of(value).filter(Objects::nonNull);
+    static <T> IsGreaterThanWhenPresent<T> isGreaterThanWhenPresent(@Nullable T value) {
+        return IsGreaterThanWhenPresent.of(value);
     }
 
-    static <T> IsGreaterThan<T> isGreaterThanWhenPresent(Supplier<T> valueSupplier) {
+    static <T> IsGreaterThanWhenPresent<T> isGreaterThanWhenPresent(Supplier<@Nullable T> valueSupplier) {
         return isGreaterThanWhenPresent(valueSupplier.get());
     }
 
@@ -698,11 +720,12 @@ static <T> IsGreaterThanOrEqualToColumn<T> isGreaterThanOrEqualTo(BasicColumn co
         return IsGreaterThanOrEqualToColumn.of(column);
     }
 
-    static <T> IsGreaterThanOrEqualTo<T> isGreaterThanOrEqualToWhenPresent(T value) {
-        return IsGreaterThanOrEqualTo.of(value).filter(Objects::nonNull);
+    static <T> IsGreaterThanOrEqualToWhenPresent<T> isGreaterThanOrEqualToWhenPresent(@Nullable T value) {
+        return IsGreaterThanOrEqualToWhenPresent.of(value);
     }
 
-    static <T> IsGreaterThanOrEqualTo<T> isGreaterThanOrEqualToWhenPresent(Supplier<T> valueSupplier) {
+    static <T> IsGreaterThanOrEqualToWhenPresent<T> isGreaterThanOrEqualToWhenPresent(
+            Supplier<@Nullable T> valueSupplier) {
         return isGreaterThanOrEqualToWhenPresent(valueSupplier.get());
     }
 
@@ -722,11 +745,11 @@ static <T> IsLessThanColumn<T> isLessThan(BasicColumn column) {
         return IsLessThanColumn.of(column);
     }
 
-    static <T> IsLessThan<T> isLessThanWhenPresent(T value) {
-        return IsLessThan.of(value).filter(Objects::nonNull);
+    static <T> IsLessThanWhenPresent<T> isLessThanWhenPresent(@Nullable T value) {
+        return IsLessThanWhenPresent.of(value);
     }
 
-    static <T> IsLessThan<T> isLessThanWhenPresent(Supplier<T> valueSupplier) {
+    static <T> IsLessThanWhenPresent<T> isLessThanWhenPresent(Supplier<@Nullable T> valueSupplier) {
         return isLessThanWhenPresent(valueSupplier.get());
     }
 
@@ -746,20 +769,20 @@ static <T> IsLessThanOrEqualToColumn<T> isLessThanOrEqualTo(BasicColumn column)
         return IsLessThanOrEqualToColumn.of(column);
     }
 
-    static <T> IsLessThanOrEqualTo<T> isLessThanOrEqualToWhenPresent(T value) {
-        return IsLessThanOrEqualTo.of(value).filter(Objects::nonNull);
+    static <T> IsLessThanOrEqualToWhenPresent<T> isLessThanOrEqualToWhenPresent(@Nullable T value) {
+        return IsLessThanOrEqualToWhenPresent.of(value);
     }
 
-    static <T> IsLessThanOrEqualTo<T> isLessThanOrEqualToWhenPresent(Supplier<T> valueSupplier) {
+    static <T> IsLessThanOrEqualToWhenPresent<T> isLessThanOrEqualToWhenPresent(Supplier<@Nullable T> valueSupplier) {
         return isLessThanOrEqualToWhenPresent(valueSupplier.get());
     }
 
     @SafeVarargs
-    static <T> IsIn<T> isIn(T... values) {
+    static <T> IsIn<T> isIn(@NonNull T... values) {
         return IsIn.of(values);
     }
 
-    static <T> IsIn<T> isIn(Collection<T> values) {
+    static <T> IsIn<T> isIn(Collection<@NonNull T> values) {
         return IsIn.of(values);
     }
 
@@ -768,20 +791,20 @@ static <T> IsInWithSubselect<T> isIn(Buildable<SelectModel> selectModelBuilder)
     }
 
     @SafeVarargs
-    static <T> IsInWhenPresent<T> isInWhenPresent(T... values) {
+    static <T> IsInWhenPresent<T> isInWhenPresent(@Nullable T... values) {
         return IsInWhenPresent.of(values);
     }
 
-    static <T> IsInWhenPresent<T> isInWhenPresent(Collection<T> values) {
-        return values == null ? IsInWhenPresent.empty() : IsInWhenPresent.of(values);
+    static <T> IsInWhenPresent<T> isInWhenPresent(@Nullable Collection<@Nullable T> values) {
+        return IsInWhenPresent.of(values);
     }
 
     @SafeVarargs
-    static <T> IsNotIn<T> isNotIn(T... values) {
+    static <T> IsNotIn<T> isNotIn(@NonNull T... values) {
         return IsNotIn.of(values);
     }
 
-    static <T> IsNotIn<T> isNotIn(Collection<T> values) {
+    static <T> IsNotIn<T> isNotIn(Collection<@NonNull T> values) {
         return IsNotIn.of(values);
     }
 
@@ -790,27 +813,27 @@ static <T> IsNotInWithSubselect<T> isNotIn(Buildable<SelectModel> selectModelBui
     }
 
     @SafeVarargs
-    static <T> IsNotInWhenPresent<T> isNotInWhenPresent(T... values) {
+    static <T> IsNotInWhenPresent<T> isNotInWhenPresent(@Nullable T... values) {
         return IsNotInWhenPresent.of(values);
     }
 
-    static <T> IsNotInWhenPresent<T> isNotInWhenPresent(Collection<T> values) {
-        return values == null ? IsNotInWhenPresent.empty() : IsNotInWhenPresent.of(values);
+    static <T> IsNotInWhenPresent<T> isNotInWhenPresent(@Nullable Collection<@Nullable T> values) {
+        return IsNotInWhenPresent.of(values);
     }
 
     static <T> IsBetween.Builder<T> isBetween(T value1) {
         return IsBetween.isBetween(value1);
     }
 
-    static <T> IsBetween.Builder<T> isBetween(Supplier<T> valueSupplier1) {
+    static <T> IsBetween.Builder<T> isBetween(Supplier<@NonNull T> valueSupplier1) {
         return isBetween(valueSupplier1.get());
     }
 
-    static <T> IsBetween.WhenPresentBuilder<T> isBetweenWhenPresent(T value1) {
-        return IsBetween.isBetweenWhenPresent(value1);
+    static <T> IsBetweenWhenPresent.Builder<T> isBetweenWhenPresent(@Nullable T value1) {
+        return IsBetweenWhenPresent.isBetweenWhenPresent(value1);
     }
 
-    static <T> IsBetween.WhenPresentBuilder<T> isBetweenWhenPresent(Supplier<T> valueSupplier1) {
+    static <T> IsBetweenWhenPresent.Builder<T> isBetweenWhenPresent(Supplier<@Nullable T> valueSupplier1) {
         return isBetweenWhenPresent(valueSupplier1.get());
     }
 
@@ -818,15 +841,15 @@ static <T> IsNotBetween.Builder<T> isNotBetween(T value1) {
         return IsNotBetween.isNotBetween(value1);
     }
 
-    static <T> IsNotBetween.Builder<T> isNotBetween(Supplier<T> valueSupplier1) {
+    static <T> IsNotBetween.Builder<T> isNotBetween(Supplier<@NonNull T> valueSupplier1) {
         return isNotBetween(valueSupplier1.get());
     }
 
-    static <T> IsNotBetween.WhenPresentBuilder<T> isNotBetweenWhenPresent(T value1) {
-        return IsNotBetween.isNotBetweenWhenPresent(value1);
+    static <T> IsNotBetweenWhenPresent.Builder<T> isNotBetweenWhenPresent(@Nullable T value1) {
+        return IsNotBetweenWhenPresent.isNotBetweenWhenPresent(value1);
     }
 
-    static <T> IsNotBetween.WhenPresentBuilder<T> isNotBetweenWhenPresent(Supplier<T> valueSupplier1) {
+    static <T> IsNotBetweenWhenPresent.Builder<T> isNotBetweenWhenPresent(Supplier<@Nullable T> valueSupplier1) {
         return isNotBetweenWhenPresent(valueSupplier1.get());
     }
 
@@ -839,11 +862,11 @@ static <T> IsLike<T> isLike(Supplier<T> valueSupplier) {
         return isLike(valueSupplier.get());
     }
 
-    static <T> IsLike<T> isLikeWhenPresent(T value) {
-        return IsLike.of(value).filter(Objects::nonNull);
+    static <T> IsLikeWhenPresent<T> isLikeWhenPresent(@Nullable T value) {
+        return IsLikeWhenPresent.of(value);
     }
 
-    static <T> IsLike<T> isLikeWhenPresent(Supplier<T> valueSupplier) {
+    static <T> IsLikeWhenPresent<T> isLikeWhenPresent(Supplier<@Nullable T> valueSupplier) {
         return isLikeWhenPresent(valueSupplier.get());
     }
 
@@ -855,11 +878,11 @@ static <T> IsNotLike<T> isNotLike(Supplier<T> valueSupplier) {
         return isNotLike(valueSupplier.get());
     }
 
-    static <T> IsNotLike<T> isNotLikeWhenPresent(T value) {
-        return IsNotLike.of(value).filter(Objects::nonNull);
+    static <T> IsNotLikeWhenPresent<T> isNotLikeWhenPresent(@Nullable T value) {
+        return IsNotLikeWhenPresent.of(value);
     }
 
-    static <T> IsNotLike<T> isNotLikeWhenPresent(Supplier<T> valueSupplier) {
+    static <T> IsNotLikeWhenPresent<T> isNotLikeWhenPresent(Supplier<@Nullable T> valueSupplier) {
         return isNotLikeWhenPresent(valueSupplier.get());
     }
 
@@ -873,69 +896,72 @@ static IsEqualTo<Boolean> isFalse() {
     }
 
     // conditions for strings only
-    static IsLikeCaseInsensitive isLikeCaseInsensitive(String value) {
+    static IsLikeCaseInsensitive<String> isLikeCaseInsensitive(String value) {
         return IsLikeCaseInsensitive.of(value);
     }
 
-    static IsLikeCaseInsensitive isLikeCaseInsensitive(Supplier<String> valueSupplier) {
+    static IsLikeCaseInsensitive<String> isLikeCaseInsensitive(Supplier<String> valueSupplier) {
         return isLikeCaseInsensitive(valueSupplier.get());
     }
 
-    static IsLikeCaseInsensitive isLikeCaseInsensitiveWhenPresent(String value) {
-        return IsLikeCaseInsensitive.of(value).filter(Objects::nonNull);
+    static IsLikeCaseInsensitiveWhenPresent<String> isLikeCaseInsensitiveWhenPresent(@Nullable String value) {
+        return IsLikeCaseInsensitiveWhenPresent.of(value);
     }
 
-    static IsLikeCaseInsensitive isLikeCaseInsensitiveWhenPresent(Supplier<String> valueSupplier) {
+    static IsLikeCaseInsensitiveWhenPresent<String> isLikeCaseInsensitiveWhenPresent(
+            Supplier<@Nullable String> valueSupplier) {
         return isLikeCaseInsensitiveWhenPresent(valueSupplier.get());
     }
 
-    static IsNotLikeCaseInsensitive isNotLikeCaseInsensitive(String value) {
+    static IsNotLikeCaseInsensitive<String> isNotLikeCaseInsensitive(String value) {
         return IsNotLikeCaseInsensitive.of(value);
     }
 
-    static IsNotLikeCaseInsensitive isNotLikeCaseInsensitive(Supplier<String> valueSupplier) {
+    static IsNotLikeCaseInsensitive<String> isNotLikeCaseInsensitive(Supplier<String> valueSupplier) {
         return isNotLikeCaseInsensitive(valueSupplier.get());
     }
 
-    static IsNotLikeCaseInsensitive isNotLikeCaseInsensitiveWhenPresent(String value) {
-        return IsNotLikeCaseInsensitive.of(value).filter(Objects::nonNull);
+    static IsNotLikeCaseInsensitiveWhenPresent<String> isNotLikeCaseInsensitiveWhenPresent(@Nullable String value) {
+        return IsNotLikeCaseInsensitiveWhenPresent.of(value);
     }
 
-    static IsNotLikeCaseInsensitive isNotLikeCaseInsensitiveWhenPresent(Supplier<String> valueSupplier) {
+    static IsNotLikeCaseInsensitiveWhenPresent<String> isNotLikeCaseInsensitiveWhenPresent(
+            Supplier<@Nullable String> valueSupplier) {
         return isNotLikeCaseInsensitiveWhenPresent(valueSupplier.get());
     }
 
-    static IsInCaseInsensitive isInCaseInsensitive(String... values) {
+    static IsInCaseInsensitive<String> isInCaseInsensitive(String... values) {
         return IsInCaseInsensitive.of(values);
     }
 
-    static IsInCaseInsensitive isInCaseInsensitive(Collection<String> values) {
+    static IsInCaseInsensitive<String> isInCaseInsensitive(Collection<String> values) {
         return IsInCaseInsensitive.of(values);
     }
 
-    static IsInCaseInsensitiveWhenPresent isInCaseInsensitiveWhenPresent(String... values) {
+    static IsInCaseInsensitiveWhenPresent<String> isInCaseInsensitiveWhenPresent(@Nullable String... values) {
         return IsInCaseInsensitiveWhenPresent.of(values);
     }
 
-    static IsInCaseInsensitiveWhenPresent isInCaseInsensitiveWhenPresent(Collection<String> values) {
-        return values == null ? IsInCaseInsensitiveWhenPresent.empty() : IsInCaseInsensitiveWhenPresent.of(values);
+    static IsInCaseInsensitiveWhenPresent<String> isInCaseInsensitiveWhenPresent(
+            @Nullable Collection<@Nullable String> values) {
+        return IsInCaseInsensitiveWhenPresent.of(values);
     }
 
-    static IsNotInCaseInsensitive isNotInCaseInsensitive(String... values) {
+    static IsNotInCaseInsensitive<String> isNotInCaseInsensitive(String... values) {
         return IsNotInCaseInsensitive.of(values);
     }
 
-    static IsNotInCaseInsensitive isNotInCaseInsensitive(Collection<String> values) {
+    static IsNotInCaseInsensitive<String> isNotInCaseInsensitive(Collection<String> values) {
         return IsNotInCaseInsensitive.of(values);
     }
 
-    static IsNotInCaseInsensitiveWhenPresent isNotInCaseInsensitiveWhenPresent(String... values) {
+    static IsNotInCaseInsensitiveWhenPresent<String> isNotInCaseInsensitiveWhenPresent(@Nullable String... values) {
         return IsNotInCaseInsensitiveWhenPresent.of(values);
     }
 
-    static IsNotInCaseInsensitiveWhenPresent isNotInCaseInsensitiveWhenPresent(Collection<String> values) {
-        return values == null ? IsNotInCaseInsensitiveWhenPresent.empty() :
-                IsNotInCaseInsensitiveWhenPresent.of(values);
+    static IsNotInCaseInsensitiveWhenPresent<String> isNotInCaseInsensitiveWhenPresent(
+            @Nullable Collection<@Nullable String> values) {
+        return IsNotInCaseInsensitiveWhenPresent.of(values);
     }
 
     // order by support
diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlColumn.java b/src/main/java/org/mybatis/dynamic/sql/SqlColumn.java
index 76f40e15c..4c841d86b 100644
--- a/src/main/java/org/mybatis/dynamic/sql/SqlColumn.java
+++ b/src/main/java/org/mybatis/dynamic/sql/SqlColumn.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,7 +19,7 @@
 import java.util.Objects;
 import java.util.Optional;
 
-import org.jetbrains.annotations.NotNull;
+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;
@@ -29,20 +29,20 @@ public class SqlColumn<T> implements BindableColumn<T>, SortSpecification {
 
     protected final String name;
     protected final SqlTable table;
-    protected final JDBCType jdbcType;
-    protected final boolean isDescending;
-    protected final String alias;
-    protected final String typeHandler;
-    protected final RenderingStrategy renderingStrategy;
+    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<T, ?> parameterTypeConverter;
-    protected final String tableQualifier;
-    protected final Class<T> javaType;
+    protected final @Nullable String tableQualifier;
+    protected final @Nullable Class<T> javaType;
 
     private SqlColumn(Builder<T> builder) {
         name = Objects.requireNonNull(builder.name);
         table = Objects.requireNonNull(builder.table);
         jdbcType = builder.jdbcType;
-        isDescending = builder.isDescending;
+        descendingPhrase = builder.descendingPhrase;
         alias = builder.alias;
         typeHandler = builder.typeHandler;
         renderingStrategy = builder.renderingStrategy;
@@ -80,14 +80,14 @@ public Optional<Class<T>> javaType() {
     }
 
     @Override
-    public Object convertParameterType(T value) {
-        return parameterTypeConverter.convert(value);
+    public @Nullable Object convertParameterType(@Nullable T value) {
+        return value == null ? null : parameterTypeConverter.convert(value);
     }
 
     @Override
     public SortSpecification descending() {
         Builder<T> b = copy();
-        return b.withDescending(true).build();
+        return b.withDescendingPhrase(" DESC").build(); //$NON-NLS-1$
     }
 
     @Override
@@ -126,13 +126,8 @@ public SqlColumn<T> asCamelCase() {
     }
 
     @Override
-    public boolean isDescending() {
-        return isDescending;
-    }
-
-    @Override
-    public String orderByName() {
-        return alias().orElse(name);
+    public FragmentAndParameters renderForOrderBy(RenderingContext renderingContext) {
+        return FragmentAndParameters.fromFragment(alias().orElse(name) + descendingPhrase);
     }
 
     @Override
@@ -149,25 +144,21 @@ public Optional<RenderingStrategy> renderingStrategy() {
         return Optional.ofNullable(renderingStrategy);
     }
 
-    @NotNull
     public <S> SqlColumn<S> withTypeHandler(String typeHandler) {
         Builder<S> b = copy();
         return b.withTypeHandler(typeHandler).build();
     }
 
-    @NotNull
     public <S> SqlColumn<S> withRenderingStrategy(RenderingStrategy renderingStrategy) {
         Builder<S> b = copy();
         return b.withRenderingStrategy(renderingStrategy).build();
     }
 
-    @NotNull
     public <S> SqlColumn<S> withParameterTypeConverter(ParameterTypeConverter<S, ?> parameterTypeConverter) {
         Builder<S> b = copy();
         return b.withParameterTypeConverter(parameterTypeConverter).build();
     }
 
-    @NotNull
     public <S> SqlColumn<S> withJavaType(Class<S> javaType) {
         Builder<S> b = copy();
         return b.withJavaType(javaType).build();
@@ -188,7 +179,7 @@ private <S> Builder<S> copy() {
                 .withName(this.name)
                 .withTable(this.table)
                 .withJdbcType(this.jdbcType)
-                .withDescending(this.isDescending)
+                .withDescendingPhrase(this.descendingPhrase)
                 .withAlias(this.alias)
                 .withTypeHandler(this.typeHandler)
                 .withRenderingStrategy(this.renderingStrategy)
@@ -211,16 +202,16 @@ public static <T> SqlColumn<T> of(String name, SqlTable table, JDBCType jdbcType
     }
 
     public static class Builder<T> {
-        protected String name;
-        protected SqlTable table;
-        protected JDBCType jdbcType;
-        protected boolean isDescending = false;
-        protected String alias;
-        protected String typeHandler;
-        protected RenderingStrategy renderingStrategy;
+        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<T, ?> parameterTypeConverter = v -> v;
-        protected String tableQualifier;
-        protected Class<T> javaType;
+        protected @Nullable String tableQualifier;
+        protected @Nullable Class<T> javaType;
 
         public Builder<T> withName(String name) {
             this.name = name;
@@ -232,27 +223,27 @@ public Builder<T> withTable(SqlTable table) {
             return this;
         }
 
-        public Builder<T> withJdbcType(JDBCType jdbcType) {
+        public Builder<T> withJdbcType(@Nullable JDBCType jdbcType) {
             this.jdbcType = jdbcType;
             return this;
         }
 
-        public Builder<T> withDescending(boolean isDescending) {
-            this.isDescending = isDescending;
+        public Builder<T> withDescendingPhrase(String descendingPhrase) {
+            this.descendingPhrase = descendingPhrase;
             return this;
         }
 
-        public Builder<T> withAlias(String alias) {
+        public Builder<T> withAlias(@Nullable String alias) {
             this.alias = alias;
             return this;
         }
 
-        public Builder<T> withTypeHandler(String typeHandler) {
+        public Builder<T> withTypeHandler(@Nullable String typeHandler) {
             this.typeHandler = typeHandler;
             return this;
         }
 
-        public Builder<T> withRenderingStrategy(RenderingStrategy renderingStrategy) {
+        public Builder<T> withRenderingStrategy(@Nullable RenderingStrategy renderingStrategy) {
             this.renderingStrategy = renderingStrategy;
             return this;
         }
@@ -262,12 +253,12 @@ public Builder<T> withParameterTypeConverter(ParameterTypeConverter<T, ?> parame
             return this;
         }
 
-        private Builder<T> withTableQualifier(String tableQualifier) {
+        private Builder<T> withTableQualifier(@Nullable String tableQualifier) {
             this.tableQualifier = tableQualifier;
             return this;
         }
 
-        public Builder<T> withJavaType(Class<T> javaType) {
+        public Builder<T> withJavaType(@Nullable Class<T> javaType) {
             this.javaType = javaType;
             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 ee3ddf414..2989f3125 100644
--- a/src/main/java/org/mybatis/dynamic/sql/SqlCriterion.java
+++ b/src/main/java/org/mybatis/dynamic/sql/SqlCriterion.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlCriterionVisitor.java b/src/main/java/org/mybatis/dynamic/sql/SqlCriterionVisitor.java
index d6ef389ec..8431568b1 100644
--- a/src/main/java/org/mybatis/dynamic/sql/SqlCriterionVisitor.java
+++ b/src/main/java/org/mybatis/dynamic/sql/SqlCriterionVisitor.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlTable.java b/src/main/java/org/mybatis/dynamic/sql/SqlTable.java
index d3b34eff0..fab3d8c2a 100644
--- a/src/main/java/org/mybatis/dynamic/sql/SqlTable.java
+++ b/src/main/java/org/mybatis/dynamic/sql/SqlTable.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,110 +18,31 @@
 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 implements TableExpression {
 
-    protected Supplier<String> nameSupplier;
+    protected String tableName;
 
     protected SqlTable(String tableName) {
-        Objects.requireNonNull(tableName);
-
-        this.nameSupplier = () -> tableName;
-    }
-
-    /**
-     * Creates an SqlTable whose name can be changed at runtime.
-     *
-     * @param tableNameSupplier table name supplier
-     * @deprecated please use {@link AliasableSqlTable} if you need to change the table name at runtime
-     */
-    @Deprecated
-    protected SqlTable(Supplier<String> tableNameSupplier) {
-        Objects.requireNonNull(tableNameSupplier);
-
-        this.nameSupplier = tableNameSupplier;
-    }
-
-    /**
-     * Creates an SqlTable whose name can be changed at runtime.
-     *
-     * @param schemaSupplier schema supplier
-     * @param tableName table name
-     * @deprecated please use {@link AliasableSqlTable} if you need to change the table name at runtime
-     */
-    @Deprecated
-    protected SqlTable(Supplier<Optional<String>> schemaSupplier, String tableName) {
-        this(Optional::empty, schemaSupplier, tableName);
-    }
-
-    /**
-     * Creates an SqlTable whose name can be changed at runtime.
-     *
-     * @param catalogSupplier catalog supplier
-     * @param schemaSupplier schema supplier
-     * @param tableName table name
-     * @deprecated please use {@link AliasableSqlTable} if you need to change the table name at runtime
-     */
-    @Deprecated
-    protected SqlTable(Supplier<Optional<String>> catalogSupplier, Supplier<Optional<String>> schemaSupplier,
-            String tableName) {
-        Objects.requireNonNull(catalogSupplier);
-        Objects.requireNonNull(schemaSupplier);
-        Objects.requireNonNull(tableName);
-
-        this.nameSupplier = () -> compose(catalogSupplier, schemaSupplier, tableName);
-    }
-
-    private String compose(Supplier<Optional<String>> catalogSupplier, Supplier<Optional<String>> schemaSupplier,
-            String tableName) {
-        return catalogSupplier.get().map(c -> compose(c, schemaSupplier, tableName))
-                .orElseGet(() -> compose(schemaSupplier, tableName));
-    }
-
-    private String compose(String catalog, Supplier<Optional<String>> schemaSupplier, String tableName) {
-        return schemaSupplier.get().map(s -> composeCatalogSchemaAndTable(catalog, s, tableName))
-                .orElseGet(() -> composeCatalogAndTable(catalog, tableName));
-    }
-
-    private String compose(Supplier<Optional<String>> 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$
-    }
-
-    private String composeSchemaAndTable(String schema, String tableName) {
-        return schema + "." + tableName; //$NON-NLS-1$
-    }
-
-    private String composeCatalogSchemaAndTable(String catalog, String schema, String tableName) {
-        return catalog + "." + schema + "." + tableName; //$NON-NLS-1$ //$NON-NLS-2$
+        this.tableName = Objects.requireNonNull(tableName);
     }
 
-    public String tableNameAtRuntime() {
-        return nameSupplier.get();
+    public String tableName() {
+        return tableName;
     }
 
     public BasicColumn allColumns() {
         return SqlColumn.of("*", this); //$NON-NLS-1$
     }
 
-    @NotNull
     public <T> SqlColumn<T> column(String name) {
         return SqlColumn.of(name, this);
     }
 
-    @NotNull
     public <T> SqlColumn<T> column(String name, JDBCType jdbcType) {
         return SqlColumn.of(name, this, jdbcType);
     }
 
-    @NotNull
     public <T> SqlColumn<T> column(String name, JDBCType jdbcType, String typeHandler) {
         SqlColumn<T> column = SqlColumn.of(name, this, jdbcType);
         return column.withTypeHandler(typeHandler);
diff --git a/src/main/java/org/mybatis/dynamic/sql/StringConstant.java b/src/main/java/org/mybatis/dynamic/sql/StringConstant.java
index bced8b446..ae9f0a75b 100644
--- a/src/main/java/org/mybatis/dynamic/sql/StringConstant.java
+++ b/src/main/java/org/mybatis/dynamic/sql/StringConstant.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,20 +18,21 @@
 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;
 import org.mybatis.dynamic.sql.util.StringUtilities;
 
 public class StringConstant implements BindableColumn<String> {
 
-    private final String alias;
+    private final @Nullable String alias;
     private final String value;
 
     private StringConstant(String value) {
         this(value, null);
     }
 
-    private StringConstant(String value, String alias) {
+    private StringConstant(String value, @Nullable String alias) {
         this.value = Objects.requireNonNull(value);
         this.alias = alias;
     }
diff --git a/src/main/java/org/mybatis/dynamic/sql/TableExpression.java b/src/main/java/org/mybatis/dynamic/sql/TableExpression.java
index 06699f93f..75ee1e8d4 100644
--- a/src/main/java/org/mybatis/dynamic/sql/TableExpression.java
+++ b/src/main/java/org/mybatis/dynamic/sql/TableExpression.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/main/java/org/mybatis/dynamic/sql/TableExpressionVisitor.java b/src/main/java/org/mybatis/dynamic/sql/TableExpressionVisitor.java
index bf232f325..407d7ebbb 100644
--- a/src/main/java/org/mybatis/dynamic/sql/TableExpressionVisitor.java
+++ b/src/main/java/org/mybatis/dynamic/sql/TableExpressionVisitor.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/main/java/org/mybatis/dynamic/sql/VisitableCondition.java b/src/main/java/org/mybatis/dynamic/sql/VisitableCondition.java
index f13076bfe..9969c3997 100644
--- a/src/main/java/org/mybatis/dynamic/sql/VisitableCondition.java
+++ b/src/main/java/org/mybatis/dynamic/sql/VisitableCondition.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,45 +17,20 @@
 
 import org.mybatis.dynamic.sql.render.RenderingContext;
 
-@FunctionalInterface
-public interface VisitableCondition<T> {
-    <R> R accept(ConditionVisitor<T, R> visitor);
-
-    /**
-     * 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 VisitableCondition#shouldRender(RenderingContext)}
-     * returns false.
-     */
-    default void renderingSkipped() {}
-
-    /**
-     * This method is called during rendering. Its purpose is to allow conditions to change
-     * the value of the rendered left column. This is primarily used in the case-insensitive conditions
-     * where we surround the rendered column with "upper(" and ")".
-     *
-     * @param renderedLeftColumn the rendered left column
-     * @return the altered column - by default no change is applied
-     */
-    default String overrideRenderedLeftColumn(String renderedLeftColumn) {
-        return renderedLeftColumn;
-    }
-}
+/**
+ * Deprecated interface.
+ *
+ * <p>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.
+ *
+ * <p>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 <code>operator</code>, <code>value</code>, etc. Subclasses of the supplied abstract conditions should continue
+ * to function as before.
+ *
+ * @param <T> 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<T> extends RenderableCondition<T> { }
diff --git a/src/main/java/org/mybatis/dynamic/sql/common/AbstractBooleanExpressionDSL.java b/src/main/java/org/mybatis/dynamic/sql/common/AbstractBooleanExpressionDSL.java
index 40f9cc0f7..2f817fb5f 100644
--- a/src/main/java/org/mybatis/dynamic/sql/common/AbstractBooleanExpressionDSL.java
+++ b/src/main/java/org/mybatis/dynamic/sql/common/AbstractBooleanExpressionDSL.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,104 +19,90 @@
 import java.util.Arrays;
 import java.util.List;
 
-import org.jetbrains.annotations.NotNull;
+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.VisitableCondition;
 import org.mybatis.dynamic.sql.util.Validator;
 
 public abstract class AbstractBooleanExpressionDSL<T extends AbstractBooleanExpressionDSL<T>> {
-    private SqlCriterion initialCriterion; // WARNING - may be null!
+    private @Nullable SqlCriterion initialCriterion;
     protected final List<AndOrCriteriaGroup> subCriteria = new ArrayList<>();
 
-    @NotNull
-    public <S> T and(BindableColumn<S> column, VisitableCondition<S> condition,
+    public <S> T and(BindableColumn<S> column, RenderableCondition<S> condition,
                      AndOrCriteriaGroup... subCriteria) {
         return and(column, condition, Arrays.asList(subCriteria));
     }
 
-    @NotNull
-    public <S> T and(BindableColumn<S> column, VisitableCondition<S> condition,
+    public <S> T and(BindableColumn<S> column, RenderableCondition<S> condition,
                      List<AndOrCriteriaGroup> subCriteria) {
         addSubCriteria("and", buildCriterion(column, condition), subCriteria); //$NON-NLS-1$
         return getThis();
     }
 
-    @NotNull
     public T and(ExistsPredicate existsPredicate, AndOrCriteriaGroup... subCriteria) {
         return and(existsPredicate, Arrays.asList(subCriteria));
     }
 
-    @NotNull
     public T and(ExistsPredicate existsPredicate, List<AndOrCriteriaGroup> subCriteria) {
         addSubCriteria("and", buildCriterion(existsPredicate), subCriteria); //$NON-NLS-1$
         return getThis();
     }
 
-    @NotNull
     public T and(SqlCriterion initialCriterion, AndOrCriteriaGroup... subCriteria) {
         return and(initialCriterion, Arrays.asList(subCriteria));
     }
 
-    @NotNull
     public T and(SqlCriterion initialCriterion, List<AndOrCriteriaGroup> subCriteria) {
         addSubCriteria("and", buildCriterion(initialCriterion), subCriteria); //$NON-NLS-1$
         return getThis();
     }
 
-    @NotNull
     public T and(List<AndOrCriteriaGroup> criteria) {
         addSubCriteria("and", criteria); //$NON-NLS-1$
         return getThis();
     }
 
-    @NotNull
-    public <S> T or(BindableColumn<S> column, VisitableCondition<S> condition,
+    public <S> T or(BindableColumn<S> column, RenderableCondition<S> condition,
                     AndOrCriteriaGroup... subCriteria) {
         return or(column, condition, Arrays.asList(subCriteria));
     }
 
-    @NotNull
-    public <S> T or(BindableColumn<S> column, VisitableCondition<S> condition,
+    public <S> T or(BindableColumn<S> column, RenderableCondition<S> condition,
                     List<AndOrCriteriaGroup> subCriteria) {
         addSubCriteria("or", buildCriterion(column, condition), subCriteria); //$NON-NLS-1$
         return getThis();
     }
 
-    @NotNull
     public T or(ExistsPredicate existsPredicate, AndOrCriteriaGroup... subCriteria) {
         return or(existsPredicate, Arrays.asList(subCriteria));
     }
 
-    @NotNull
     public T or(ExistsPredicate existsPredicate, List<AndOrCriteriaGroup> subCriteria) {
         addSubCriteria("or", buildCriterion(existsPredicate), subCriteria); //$NON-NLS-1$
         return getThis();
     }
 
-    @NotNull
     public T or(SqlCriterion initialCriterion, AndOrCriteriaGroup... subCriteria) {
         return or(initialCriterion, Arrays.asList(subCriteria));
     }
 
-    @NotNull
     public T or(SqlCriterion initialCriterion, List<AndOrCriteriaGroup> subCriteria) {
         addSubCriteria("or", buildCriterion(initialCriterion), subCriteria); //$NON-NLS-1$
         return getThis();
     }
 
-    @NotNull
     public T or(List<AndOrCriteriaGroup> criteria) {
         addSubCriteria("or", criteria); //$NON-NLS-1$
         return getThis();
     }
 
-    private <R> SqlCriterion buildCriterion(BindableColumn<R> column, VisitableCondition<R> condition) {
+    private <R> SqlCriterion buildCriterion(BindableColumn<R> column, RenderableCondition<R> condition) {
         return ColumnAndConditionCriterion.withColumn(column).withCondition(condition).build();
     }
 
@@ -144,17 +130,16 @@ private void addSubCriteria(String connector, List<AndOrCriteriaGroup> criteria)
                 .build());
     }
 
-    protected void setInitialCriterion(SqlCriterion initialCriterion) {
+    protected void setInitialCriterion(@Nullable SqlCriterion initialCriterion) {
         this.initialCriterion = initialCriterion;
     }
 
-    protected void setInitialCriterion(SqlCriterion initialCriterion, StatementType statementType) {
+    protected void setInitialCriterion(@Nullable SqlCriterion initialCriterion, StatementType statementType) {
         Validator.assertTrue(this.initialCriterion == null, statementType.messageNumber());
         setInitialCriterion(initialCriterion);
     }
 
-    // may be null!
-    protected SqlCriterion getInitialCriterion() {
+    protected @Nullable SqlCriterion getInitialCriterion() {
         return initialCriterion;
     }
 
diff --git a/src/main/java/org/mybatis/dynamic/sql/common/AbstractBooleanExpressionModel.java b/src/main/java/org/mybatis/dynamic/sql/common/AbstractBooleanExpressionModel.java
index 0913fe17c..edda8707f 100644
--- a/src/main/java/org/mybatis/dynamic/sql/common/AbstractBooleanExpressionModel.java
+++ b/src/main/java/org/mybatis/dynamic/sql/common/AbstractBooleanExpressionModel.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -20,11 +20,12 @@
 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 SqlCriterion initialCriterion;
+    private final @Nullable SqlCriterion initialCriterion;
     private final List<AndOrCriteriaGroup> subCriteria ;
 
     protected AbstractBooleanExpressionModel(AbstractBuilder<?> builder) {
@@ -41,10 +42,10 @@ public List<AndOrCriteriaGroup> subCriteria() {
     }
 
     public abstract static class AbstractBuilder<T extends AbstractBuilder<T>> {
-        private SqlCriterion initialCriterion;
+        private @Nullable SqlCriterion initialCriterion;
         private final List<AndOrCriteriaGroup> subCriteria = new ArrayList<>();
 
-        public T withInitialCriterion(SqlCriterion initialCriterion) {
+        public T withInitialCriterion(@Nullable SqlCriterion initialCriterion) {
             this.initialCriterion = initialCriterion;
             return 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
index 910e20bd3..a006b1834 100644
--- a/src/main/java/org/mybatis/dynamic/sql/common/AbstractBooleanExpressionRenderer.java
+++ b/src/main/java/org/mybatis/dynamic/sql/common/AbstractBooleanExpressionRenderer.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -21,6 +21,7 @@
 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;
@@ -84,7 +85,7 @@ private String addPrefix(String fragment) {
 
     public abstract static class AbstractBuilder<B extends AbstractBuilder<B>> {
         private final AbstractBooleanExpressionModel model;
-        private RenderingContext renderingContext;
+        private @Nullable RenderingContext renderingContext;
 
         protected AbstractBuilder(AbstractBooleanExpressionModel model) {
             this.model = model;
diff --git a/src/main/java/org/mybatis/dynamic/sql/common/CommonBuilder.java b/src/main/java/org/mybatis/dynamic/sql/common/CommonBuilder.java
index f25457398..269b857af 100644
--- a/src/main/java/org/mybatis/dynamic/sql/common/CommonBuilder.java
+++ b/src/main/java/org/mybatis/dynamic/sql/common/CommonBuilder.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,6 +15,7 @@
  */
 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;
@@ -25,34 +26,34 @@
  * @param <T> type of the implementing builder
  */
 public abstract class CommonBuilder<T extends CommonBuilder<T>> {
-    private SqlTable table;
-    private String tableAlias;
-    private EmbeddedWhereModel whereModel;
-    private Long limit;
-    private OrderByModel orderByModel;
-    private StatementConfiguration statementConfiguration;
+    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 SqlTable table() {
+    public @Nullable SqlTable table() {
         return table;
     }
 
-    public String tableAlias() {
+    public @Nullable String tableAlias() {
         return tableAlias;
     }
 
-    public EmbeddedWhereModel whereModel() {
+    public @Nullable EmbeddedWhereModel whereModel() {
         return whereModel;
     }
 
-    public Long limit() {
+    public @Nullable Long limit() {
         return limit;
     }
 
-    public OrderByModel orderByModel() {
+    public @Nullable OrderByModel orderByModel() {
         return orderByModel;
     }
 
-    public StatementConfiguration statementConfiguration() {
+    public @Nullable StatementConfiguration statementConfiguration() {
         return statementConfiguration;
     }
 
@@ -61,22 +62,22 @@ public T withTable(SqlTable table) {
         return getThis();
     }
 
-    public T withTableAlias(String tableAlias) {
+    public T withTableAlias(@Nullable String tableAlias) {
         this.tableAlias = tableAlias;
         return getThis();
     }
 
-    public T withWhereModel(EmbeddedWhereModel whereModel) {
+    public T withWhereModel(@Nullable EmbeddedWhereModel whereModel) {
         this.whereModel = whereModel;
         return getThis();
     }
 
-    public T withLimit(Long limit) {
+    public T withLimit(@Nullable Long limit) {
         this.limit = limit;
         return getThis();
     }
 
-    public T withOrderByModel(OrderByModel orderByModel) {
+    public T withOrderByModel(@Nullable OrderByModel orderByModel) {
         this.orderByModel = orderByModel;
         return getThis();
     }
diff --git a/src/main/java/org/mybatis/dynamic/sql/common/OrderByModel.java b/src/main/java/org/mybatis/dynamic/sql/common/OrderByModel.java
index 3a05f49d9..42d2de4fb 100644
--- a/src/main/java/org/mybatis/dynamic/sql/common/OrderByModel.java
+++ b/src/main/java/org/mybatis/dynamic/sql/common/OrderByModel.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/main/java/org/mybatis/dynamic/sql/common/OrderByRenderer.java b/src/main/java/org/mybatis/dynamic/sql/common/OrderByRenderer.java
index f1a28ed4e..851bf73ea 100644
--- a/src/main/java/org/mybatis/dynamic/sql/common/OrderByRenderer.java
+++ b/src/main/java/org/mybatis/dynamic/sql/common/OrderByRenderer.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,24 +15,24 @@
  */
 package org.mybatis.dynamic.sql.common;
 
+import java.util.Objects;
 import java.util.stream.Collectors;
 
-import org.mybatis.dynamic.sql.SortSpecification;
+import org.mybatis.dynamic.sql.render.RenderingContext;
 import org.mybatis.dynamic.sql.util.FragmentAndParameters;
+import org.mybatis.dynamic.sql.util.FragmentCollector;
 
 public class OrderByRenderer {
-    public FragmentAndParameters render(OrderByModel orderByModel) {
-        String phrase = orderByModel.columns()
-                .map(this::calculateOrderByPhrase)
-                .collect(Collectors.joining(", ", "order by ", "")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
-        return FragmentAndParameters.fromFragment(phrase);
+    private final RenderingContext renderingContext;
+
+    public OrderByRenderer(RenderingContext renderingContext) {
+        this.renderingContext = Objects.requireNonNull(renderingContext);
     }
 
-    private String calculateOrderByPhrase(SortSpecification column) {
-        String phrase = column.orderByName();
-        if (column.isDescending()) {
-            phrase = phrase + " DESC"; //$NON-NLS-1$
-        }
-        return phrase;
+    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/main/java/org/mybatis/dynamic/sql/select/join/JoinConditionVisitor.java b/src/main/java/org/mybatis/dynamic/sql/common/package-info.java
similarity index 70%
rename from src/main/java/org/mybatis/dynamic/sql/select/join/JoinConditionVisitor.java
rename to src/main/java/org/mybatis/dynamic/sql/common/package-info.java
index 582b332f1..f581c8034 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinConditionVisitor.java
+++ b/src/main/java/org/mybatis/dynamic/sql/common/package-info.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -13,10 +13,7 @@
  *    See the License for the specific language governing permissions and
  *    limitations under the License.
  */
-package org.mybatis.dynamic.sql.select.join;
+@NullMarked
+package org.mybatis.dynamic.sql.common;
 
-public interface JoinConditionVisitor<T, R> {
-    R visit(TypedJoinCondition<T> condition);
-
-    R visit(ColumnBasedJoinCondition<T> condition);
-}
+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
index 8e7054d78..670ddd935 100644
--- a/src/main/java/org/mybatis/dynamic/sql/configuration/GlobalConfiguration.java
+++ b/src/main/java/org/mybatis/dynamic/sql/configuration/GlobalConfiguration.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,6 +17,7 @@
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.Objects;
 import java.util.Properties;
 
 import org.mybatis.dynamic.sql.exception.DynamicSqlException;
@@ -47,11 +48,7 @@ private void initializeProperties() {
 
     private String getConfigurationFileName() {
         String property = System.getProperty(CONFIGURATION_FILE_PROPERTY);
-        if (property == null) {
-            return DEFAULT_PROPERTY_FILE;
-        } else {
-            return property;
-        }
+        return Objects.requireNonNullElse(property, DEFAULT_PROPERTY_FILE);
     }
 
     void loadProperties(InputStream inputStream, String propertyFile) {
diff --git a/src/main/java/org/mybatis/dynamic/sql/configuration/GlobalContext.java b/src/main/java/org/mybatis/dynamic/sql/configuration/GlobalContext.java
index 0cdc5d902..6963374b6 100644
--- a/src/main/java/org/mybatis/dynamic/sql/configuration/GlobalContext.java
+++ b/src/main/java/org/mybatis/dynamic/sql/configuration/GlobalContext.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/main/java/org/mybatis/dynamic/sql/configuration/StatementConfiguration.java b/src/main/java/org/mybatis/dynamic/sql/configuration/StatementConfiguration.java
index bb5641f3f..ed187fe92 100644
--- a/src/main/java/org/mybatis/dynamic/sql/configuration/StatementConfiguration.java
+++ b/src/main/java/org/mybatis/dynamic/sql/configuration/StatementConfiguration.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/EqualToValue.java b/src/main/java/org/mybatis/dynamic/sql/configuration/package-info.java
similarity index 65%
rename from src/main/java/org/mybatis/dynamic/sql/select/join/EqualToValue.java
rename to src/main/java/org/mybatis/dynamic/sql/configuration/package-info.java
index 2d038aa21..41fcac6c7 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/join/EqualToValue.java
+++ b/src/main/java/org/mybatis/dynamic/sql/configuration/package-info.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -13,15 +13,7 @@
  *    See the License for the specific language governing permissions and
  *    limitations under the License.
  */
-package org.mybatis.dynamic.sql.select.join;
+@NullMarked
+package org.mybatis.dynamic.sql.configuration;
 
-public class EqualToValue<T> extends TypedJoinCondition<T> {
-    public EqualToValue(T value) {
-        super(value);
-    }
-
-    @Override
-    public String operator() {
-        return "="; //$NON-NLS-1$
-    }
-}
+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 e7c1ad184..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -21,29 +21,28 @@
 import java.util.function.Consumer;
 import java.util.function.Function;
 
-import org.jetbrains.annotations.NotNull;
+import org.jspecify.annotations.Nullable;
 import org.mybatis.dynamic.sql.SortSpecification;
 import org.mybatis.dynamic.sql.SqlTable;
 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.Utilities;
 import org.mybatis.dynamic.sql.where.AbstractWhereFinisher;
 import org.mybatis.dynamic.sql.where.AbstractWhereStarter;
 import org.mybatis.dynamic.sql.where.EmbeddedWhereModel;
 
-public class DeleteDSL<R> extends AbstractWhereStarter<DeleteDSL<R>.DeleteWhereBuilder, DeleteDSL<R>>
-        implements Buildable<R> {
+public class DeleteDSL<R> implements AbstractWhereStarter<DeleteDSL<R>.DeleteWhereBuilder, DeleteDSL<R>>,
+        Buildable<R> {
 
     private final Function<DeleteModel, R> adapterFunction;
     private final SqlTable table;
-    private final String tableAlias;
-    private DeleteWhereBuilder whereBuilder;
+    private final @Nullable String tableAlias;
+    private @Nullable DeleteWhereBuilder whereBuilder;
     private final StatementConfiguration statementConfiguration = new StatementConfiguration();
-    private Long limit;
-    private OrderByModel orderByModel;
+    private @Nullable Long limit;
+    private @Nullable OrderByModel orderByModel;
 
-    private DeleteDSL(SqlTable table, String tableAlias, Function<DeleteModel, R> adapterFunction) {
+    private DeleteDSL(SqlTable table, @Nullable String tableAlias, Function<DeleteModel, R> adapterFunction) {
         this.table = Objects.requireNonNull(table);
         this.tableAlias = tableAlias;
         this.adapterFunction = Objects.requireNonNull(adapterFunction);
@@ -51,11 +50,15 @@ private DeleteDSL(SqlTable table, String tableAlias, Function<DeleteModel, R> ad
 
     @Override
     public DeleteWhereBuilder where() {
-        whereBuilder = Utilities.buildIfNecessary(whereBuilder, DeleteWhereBuilder::new);
+        whereBuilder = Objects.requireNonNullElseGet(whereBuilder, DeleteWhereBuilder::new);
         return whereBuilder;
     }
 
     public DeleteDSL<R> limit(long limit) {
+        return limitWhenPresent(limit);
+    }
+
+    public DeleteDSL<R> limitWhenPresent(@Nullable Long limit) {
         this.limit = limit;
         return this;
     }
@@ -75,7 +78,6 @@ public DeleteDSL<R> orderBy(Collection<? extends SortSpecification> columns) {
      *
      * @return the model class
      */
-    @NotNull
     @Override
     public R build() {
         DeleteModel deleteModel = DeleteModel.withTable(table)
@@ -96,7 +98,7 @@ public DeleteDSL<R> configureStatement(Consumer<StatementConfiguration> consumer
     }
 
     public static <R> DeleteDSL<R> deleteFrom(Function<DeleteModel, R> adapterFunction, SqlTable table,
-            String tableAlias) {
+            @Nullable String tableAlias) {
         return new DeleteDSL<>(table, tableAlias, adapterFunction);
     }
 
@@ -115,7 +117,11 @@ private DeleteWhereBuilder() {
         }
 
         public DeleteDSL<R> limit(long limit) {
-            return DeleteDSL.this.limit(limit);
+            return limitWhenPresent(limit);
+        }
+
+        public DeleteDSL<R> limitWhenPresent(@Nullable Long limit) {
+            return DeleteDSL.this.limitWhenPresent(limit);
         }
 
         public DeleteDSL<R> orderBy(SortSpecification... columns) {
@@ -127,7 +133,6 @@ public DeleteDSL<R> orderBy(Collection<? extends SortSpecification> columns) {
             return DeleteDSL.this;
         }
 
-        @NotNull
         @Override
         public R build() {
             return DeleteDSL.this.build();
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 799ea7e5e..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 54770d5d4..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,7 +18,7 @@
 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;
@@ -30,10 +30,10 @@
 
 public class DeleteModel {
     private final SqlTable table;
-    private final String tableAlias;
-    private final EmbeddedWhereModel whereModel;
-    private final Long limit;
-    private final OrderByModel orderByModel;
+    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) {
@@ -65,11 +65,13 @@ public Optional<OrderByModel> orderByModel() {
         return Optional.ofNullable(orderByModel);
     }
 
-    @NotNull
+    public StatementConfiguration statementConfiguration() {
+        return statementConfiguration;
+    }
+
     public DeleteStatementProvider render(RenderingStrategy renderingStrategy) {
         return DeleteRenderer.withDeleteModel(this)
                 .withRenderingStrategy(renderingStrategy)
-                .withStatementConfiguration(statementConfiguration)
                 .build()
                 .render();
     }
diff --git a/src/test/java/examples/schema_supplier/SchemaSupplier.java b/src/main/java/org/mybatis/dynamic/sql/delete/package-info.java
similarity index 62%
rename from src/test/java/examples/schema_supplier/SchemaSupplier.java
rename to src/main/java/org/mybatis/dynamic/sql/delete/package-info.java
index b5a3e8fbe..a7bfc9d26 100644
--- a/src/test/java/examples/schema_supplier/SchemaSupplier.java
+++ b/src/main/java/org/mybatis/dynamic/sql/delete/package-info.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -13,14 +13,7 @@
  *    See the License for the specific language governing permissions and
  *    limitations under the License.
  */
-package examples.schema_supplier;
+@NullMarked
+package org.mybatis.dynamic.sql.delete;
 
-import java.util.Optional;
-
-public class SchemaSupplier {
-    public static final String schema_property = "schemaToUse";
-
-    public static Optional<String> schemaPropertyReader() {
-        return Optional.ofNullable(System.getProperty(schema_property));
-    }
-}
+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 89b72333b..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,13 +19,15 @@
 import java.util.Map;
 import java.util.Objects;
 
+import org.jspecify.annotations.Nullable;
+
 public class DefaultDeleteStatementProvider implements DeleteStatementProvider {
     private final String deleteStatement;
     private final Map<String, Object> parameters;
 
     private DefaultDeleteStatementProvider(Builder builder) {
         deleteStatement = Objects.requireNonNull(builder.deleteStatement);
-        parameters = Objects.requireNonNull(builder.parameters);
+        parameters = builder.parameters;
     }
 
     @Override
@@ -43,7 +45,7 @@ public static Builder withDeleteStatement(String deleteStatement) {
     }
 
     public static class Builder {
-        private String deleteStatement;
+        private @Nullable String deleteStatement;
         private final Map<String, Object> parameters = new HashMap<>();
 
         public Builder withDeleteStatement(String deleteStatement) {
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 ce46c4c10..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,9 +19,9 @@
 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.configuration.StatementConfiguration;
 import org.mybatis.dynamic.sql.delete.DeleteModel;
 import org.mybatis.dynamic.sql.render.ExplicitTableAliasCalculator;
 import org.mybatis.dynamic.sql.render.RenderedParameterInfo;
@@ -44,7 +44,7 @@ private DeleteRenderer(Builder builder) {
         renderingContext = RenderingContext
                 .withRenderingStrategy(Objects.requireNonNull(builder.renderingStrategy))
                 .withTableAliasCalculator(tableAliasCalculator)
-                .withStatementConfiguration(builder.statementConfiguration)
+                .withStatementConfiguration(deleteModel.statementConfiguration())
                 .build();
     }
 
@@ -84,7 +84,7 @@ private Optional<FragmentAndParameters> calculateLimitClause() {
     }
 
     private FragmentAndParameters renderLimitClause(Long limit) {
-        RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo();
+        RenderedParameterInfo parameterInfo = renderingContext.calculateLimitParameterInfo();
 
         return FragmentAndParameters.withFragment("limit " + parameterInfo.renderedPlaceHolder()) //$NON-NLS-1$
                 .withParameter(parameterInfo.parameterMapKey(), limit)
@@ -96,7 +96,7 @@ private Optional<FragmentAndParameters> calculateOrderByClause() {
     }
 
     private FragmentAndParameters renderOrderByClause(OrderByModel orderByModel) {
-        return new OrderByRenderer().render(orderByModel);
+        return new OrderByRenderer(renderingContext).render(orderByModel);
     }
 
     public static Builder withDeleteModel(DeleteModel deleteModel) {
@@ -104,9 +104,8 @@ public static Builder withDeleteModel(DeleteModel deleteModel) {
     }
 
     public static class Builder {
-        private DeleteModel deleteModel;
-        private RenderingStrategy renderingStrategy;
-        private StatementConfiguration statementConfiguration;
+        private @Nullable DeleteModel deleteModel;
+        private @Nullable RenderingStrategy renderingStrategy;
 
         public Builder withDeleteModel(DeleteModel deleteModel) {
             this.deleteModel = deleteModel;
@@ -118,11 +117,6 @@ public Builder withRenderingStrategy(RenderingStrategy renderingStrategy) {
             return this;
         }
 
-        public Builder withStatementConfiguration(StatementConfiguration statementConfiguration) {
-            this.statementConfiguration = statementConfiguration;
-            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 9ccf16c98..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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
index d32e66e5e..21d43a927 100644
--- a/src/main/java/org/mybatis/dynamic/sql/exception/DuplicateTableAliasException.java
+++ b/src/main/java/org/mybatis/dynamic/sql/exception/DuplicateTableAliasException.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,6 +15,7 @@
  */
 package org.mybatis.dynamic.sql.exception;
 
+import java.io.Serial;
 import java.util.Objects;
 
 import org.mybatis.dynamic.sql.SqlTable;
@@ -34,6 +35,7 @@
  */
 public class DuplicateTableAliasException extends DynamicSqlException {
 
+    @Serial
     private static final long serialVersionUID = -2631664872557787391L;
 
     public DuplicateTableAliasException(SqlTable table, String newAlias, String existingAlias) {
@@ -43,6 +45,6 @@ public DuplicateTableAliasException(SqlTable table, String newAlias, String exis
     }
 
     private static String generateMessage(SqlTable table, String newAlias, String existingAlias) {
-        return Messages.getString("ERROR.1", table.tableNameAtRuntime(), newAlias, existingAlias); //$NON-NLS-1$
+        return Messages.getString("ERROR.1", table.tableName(), newAlias, existingAlias); //$NON-NLS-1$
     }
 }
diff --git a/src/main/java/org/mybatis/dynamic/sql/exception/DynamicSqlException.java b/src/main/java/org/mybatis/dynamic/sql/exception/DynamicSqlException.java
index 3a8bc5791..f1836b990 100644
--- a/src/main/java/org/mybatis/dynamic/sql/exception/DynamicSqlException.java
+++ b/src/main/java/org/mybatis/dynamic/sql/exception/DynamicSqlException.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,7 +15,10 @@
  */
 package org.mybatis.dynamic.sql.exception;
 
+import java.io.Serial;
+
 public class DynamicSqlException extends RuntimeException {
+    @Serial
     private static final long serialVersionUID = 349021672061361244L;
 
     public DynamicSqlException(String message) {
diff --git a/src/main/java/org/mybatis/dynamic/sql/exception/InvalidSqlException.java b/src/main/java/org/mybatis/dynamic/sql/exception/InvalidSqlException.java
index 51ce3f787..44d407ea1 100644
--- a/src/main/java/org/mybatis/dynamic/sql/exception/InvalidSqlException.java
+++ b/src/main/java/org/mybatis/dynamic/sql/exception/InvalidSqlException.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,7 +15,10 @@
  */
 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) {
diff --git a/src/main/java/org/mybatis/dynamic/sql/exception/NonRenderingWhereClauseException.java b/src/main/java/org/mybatis/dynamic/sql/exception/NonRenderingWhereClauseException.java
index 3dfa0ed36..0561f2d42 100644
--- a/src/main/java/org/mybatis/dynamic/sql/exception/NonRenderingWhereClauseException.java
+++ b/src/main/java/org/mybatis/dynamic/sql/exception/NonRenderingWhereClauseException.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,6 +15,8 @@
  */
 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;
@@ -40,6 +42,7 @@
  * @author Jeff Butler
  */
 public class NonRenderingWhereClauseException extends DynamicSqlException {
+    @Serial
     private static final long serialVersionUID = 6619119078542625135L;
 
     public NonRenderingWhereClauseException() {
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 01056f60f..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -22,6 +22,7 @@
 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.util.AbstractColumnMapping;
 
@@ -53,7 +54,7 @@ public int recordCount() {
     }
 
     public abstract static class AbstractBuilder<T, S extends AbstractBuilder<T, S>> {
-        private SqlTable table;
+        private @Nullable SqlTable table;
         private final List<T> records = new ArrayList<>();
         private final List<AbstractColumnMapping> columnMappings = new ArrayList<>();
 
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 9b5f69cec..71bc350d6 100644
--- a/src/main/java/org/mybatis/dynamic/sql/insert/BatchInsertDSL.java
+++ b/src/main/java/org/mybatis/dynamic/sql/insert/BatchInsertDSL.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -21,7 +21,7 @@
 import java.util.List;
 import java.util.Objects;
 
-import org.jetbrains.annotations.NotNull;
+import org.jspecify.annotations.Nullable;
 import org.mybatis.dynamic.sql.SqlColumn;
 import org.mybatis.dynamic.sql.SqlTable;
 import org.mybatis.dynamic.sql.util.AbstractColumnMapping;
@@ -48,7 +48,6 @@ public <F> ColumnMappingFinisher<F> map(SqlColumn<F> column) {
         return new ColumnMappingFinisher<>(column);
     }
 
-    @NotNull
     @Override
     public BatchInsertModel<T> build() {
         return BatchInsertModel.withRecords(records)
@@ -58,11 +57,11 @@ public BatchInsertModel<T> build() {
     }
 
     @SafeVarargs
-    public static <T> IntoGatherer<T> insert(T... records) {
-        return BatchInsertDSL.insert(Arrays.asList(records));
+    public static <T> BatchInsertDSL.IntoGatherer<T> insert(T... records) {
+        return insert(Arrays.asList(records));
     }
 
-    public static <T> IntoGatherer<T> insert(Collection<T> records) {
+    public static <T> BatchInsertDSL.IntoGatherer<T> insert(Collection<T> records) {
         return new IntoGatherer<>(records);
     }
 
@@ -113,7 +112,7 @@ public BatchInsertDSL<T> toRow() {
 
     public abstract static class AbstractBuilder<T, B extends AbstractBuilder<T, B>> {
         final Collection<T> records = new ArrayList<>();
-        SqlTable table;
+        @Nullable SqlTable table;
         final List<AbstractColumnMapping> columnMappings = new ArrayList<>();
 
         public B withRecords(Collection<T> records) {
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 b591753a2..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,7 +17,6 @@
 
 import java.util.Collection;
 
-import org.jetbrains.annotations.NotNull;
 import org.mybatis.dynamic.sql.insert.render.BatchInsert;
 import org.mybatis.dynamic.sql.insert.render.BatchInsertRenderer;
 import org.mybatis.dynamic.sql.render.RenderingStrategy;
@@ -31,7 +30,6 @@ private BatchInsertModel(Builder<T> builder) {
         Validator.assertNotEmpty(columnMappings, "ERROR.5"); //$NON-NLS-1$
     }
 
-    @NotNull
     public BatchInsert<T> render(RenderingStrategy renderingStrategy) {
         return BatchInsertRenderer.withBatchInsertModel(this)
                 .withRenderingStrategy(renderingStrategy)
diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/GeneralInsertDSL.java b/src/main/java/org/mybatis/dynamic/sql/insert/GeneralInsertDSL.java
index f36fd6544..5cba9063d 100644
--- a/src/main/java/org/mybatis/dynamic/sql/insert/GeneralInsertDSL.java
+++ b/src/main/java/org/mybatis/dynamic/sql/insert/GeneralInsertDSL.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -21,7 +21,7 @@
 import java.util.Objects;
 import java.util.function.Supplier;
 
-import org.jetbrains.annotations.NotNull;
+import org.jspecify.annotations.Nullable;
 import org.mybatis.dynamic.sql.SqlColumn;
 import org.mybatis.dynamic.sql.SqlTable;
 import org.mybatis.dynamic.sql.configuration.StatementConfiguration;
@@ -47,7 +47,6 @@ public <T> SetClauseFinisher<T> set(SqlColumn<T> column) {
         return new SetClauseFinisher<>(column);
     }
 
-    @NotNull
     @Override
     public GeneralInsertModel build() {
         return new GeneralInsertModel.Builder()
@@ -93,20 +92,20 @@ public GeneralInsertDSL toValue(Supplier<T> valueSupplier) {
             return GeneralInsertDSL.this;
         }
 
-        public GeneralInsertDSL toValueOrNull(T value) {
+        public GeneralInsertDSL toValueOrNull(@Nullable T value) {
             return toValueOrNull(() -> value);
         }
 
-        public GeneralInsertDSL toValueOrNull(Supplier<T> valueSupplier) {
+        public GeneralInsertDSL toValueOrNull(Supplier<@Nullable T> valueSupplier) {
             columnMappings.add(ValueOrNullMapping.of(column, valueSupplier));
             return GeneralInsertDSL.this;
         }
 
-        public GeneralInsertDSL toValueWhenPresent(T value) {
+        public GeneralInsertDSL toValueWhenPresent(@Nullable T value) {
             return toValueWhenPresent(() -> value);
         }
 
-        public GeneralInsertDSL toValueWhenPresent(Supplier<T> valueSupplier) {
+        public GeneralInsertDSL toValueWhenPresent(Supplier<@Nullable T> valueSupplier) {
             columnMappings.add(ValueWhenPresentMapping.of(column, valueSupplier));
             return GeneralInsertDSL.this;
         }
@@ -114,7 +113,7 @@ public GeneralInsertDSL toValueWhenPresent(Supplier<T> valueSupplier) {
 
     public static class Builder {
         private final List<AbstractColumnMapping> columnMappings = new ArrayList<>();
-        private SqlTable table;
+        private @Nullable SqlTable table;
 
         public Builder withTable(SqlTable table) {
             this.table = table;
diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/GeneralInsertModel.java b/src/main/java/org/mybatis/dynamic/sql/insert/GeneralInsertModel.java
index 7f219027d..873c98f5f 100644
--- a/src/main/java/org/mybatis/dynamic/sql/insert/GeneralInsertModel.java
+++ b/src/main/java/org/mybatis/dynamic/sql/insert/GeneralInsertModel.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -20,7 +20,7 @@
 import java.util.Objects;
 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.configuration.StatementConfiguration;
 import org.mybatis.dynamic.sql.insert.render.GeneralInsertRenderer;
@@ -50,19 +50,21 @@ public SqlTable table() {
         return table;
     }
 
-    @NotNull
+    public StatementConfiguration statementConfiguration() {
+        return statementConfiguration;
+    }
+
     public GeneralInsertStatementProvider render(RenderingStrategy renderingStrategy) {
         return GeneralInsertRenderer.withInsertModel(this)
                 .withRenderingStrategy(renderingStrategy)
-                .withStatementConfiguration(statementConfiguration)
                 .build()
                 .render();
     }
 
     public static class Builder {
-        private SqlTable table;
+        private @Nullable SqlTable table;
         private final List<AbstractColumnMapping> insertMappings = new ArrayList<>();
-        private StatementConfiguration statementConfiguration;
+        private @Nullable StatementConfiguration statementConfiguration;
 
         public Builder withTable(SqlTable table) {
             this.table = table;
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 818868573..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -20,13 +20,14 @@
 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 final List<SqlColumn<?>> columns = new ArrayList<>();
 
-    private InsertColumnListModel(List<SqlColumn<?>> columns) {
+    private InsertColumnListModel(@Nullable List<SqlColumn<?>> columns) {
         Objects.requireNonNull(columns);
         Validator.assertNotEmpty(columns, "ERROR.4"); //$NON-NLS-1$
         this.columns.addAll(columns);
@@ -37,7 +38,7 @@ public Stream<SqlColumn<?>> columns() {
         return columns.stream();
     }
 
-    public static InsertColumnListModel of(List<SqlColumn<?>> columns) {
+    public static InsertColumnListModel of(@Nullable List<SqlColumn<?>> 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 73ac1e71a..d622ced8c 100644
--- a/src/main/java/org/mybatis/dynamic/sql/insert/InsertDSL.java
+++ b/src/main/java/org/mybatis/dynamic/sql/insert/InsertDSL.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -21,7 +21,7 @@
 import java.util.Objects;
 import java.util.function.Supplier;
 
-import org.jetbrains.annotations.NotNull;
+import org.jspecify.annotations.Nullable;
 import org.mybatis.dynamic.sql.SqlColumn;
 import org.mybatis.dynamic.sql.SqlTable;
 import org.mybatis.dynamic.sql.util.AbstractColumnMapping;
@@ -49,7 +49,6 @@ public <F> ColumnMappingFinisher<F> map(SqlColumn<F> column) {
         return new ColumnMappingFinisher<>(column);
     }
 
-    @NotNull
     @Override
     public InsertModel<T> build() {
         return InsertModel.withRow(row)
@@ -113,8 +112,8 @@ public InsertDSL<T> toRow() {
     }
 
     public static class Builder<T> {
-        private T row;
-        private SqlTable table;
+        private @Nullable T row;
+        private @Nullable SqlTable table;
         private final List<AbstractColumnMapping> columnMappings = new ArrayList<>();
 
         public Builder<T> withRow(T row) {
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 7f740051e..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -20,7 +20,7 @@
 import java.util.Objects;
 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.insert.render.InsertRenderer;
 import org.mybatis.dynamic.sql.insert.render.InsertStatementProvider;
@@ -52,7 +52,6 @@ public SqlTable table() {
         return table;
     }
 
-    @NotNull
     public InsertStatementProvider<T> render(RenderingStrategy renderingStrategy) {
         return InsertRenderer.withInsertModel(this)
                 .withRenderingStrategy(renderingStrategy)
@@ -65,8 +64,8 @@ public static <T> Builder<T> withRow(T row) {
     }
 
     public static class Builder<T> {
-        private SqlTable table;
-        private T row;
+        private @Nullable SqlTable table;
+        private @Nullable T row;
         private final List<AbstractColumnMapping> columnMappings = new ArrayList<>();
 
         public Builder<T> withTable(SqlTable table) {
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 20fad06ad..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -20,7 +20,7 @@
 import java.util.Objects;
 import java.util.function.Consumer;
 
-import org.jetbrains.annotations.NotNull;
+import org.jspecify.annotations.Nullable;
 import org.mybatis.dynamic.sql.SqlColumn;
 import org.mybatis.dynamic.sql.SqlTable;
 import org.mybatis.dynamic.sql.configuration.StatementConfiguration;
@@ -31,7 +31,7 @@
 public class InsertSelectDSL implements Buildable<InsertSelectModel>, ConfigurableStatement<InsertSelectDSL> {
 
     private final SqlTable table;
-    private final InsertColumnListModel columnList;
+    private final @Nullable InsertColumnListModel columnList;
     private final SelectModel selectModel;
     private final StatementConfiguration statementConfiguration = new StatementConfiguration();
 
@@ -47,7 +47,6 @@ private InsertSelectDSL(SqlTable table, SelectModel selectModel) {
         this.columnList = null;
     }
 
-    @NotNull
     @Override
     public InsertSelectModel build() {
         return InsertSelectModel.withTable(table)
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 4da2cef1c..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,7 +18,7 @@
 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.configuration.StatementConfiguration;
 import org.mybatis.dynamic.sql.insert.render.InsertSelectRenderer;
@@ -28,7 +28,7 @@
 
 public class InsertSelectModel {
     private final SqlTable table;
-    private final InsertColumnListModel columnList;
+    private final @Nullable InsertColumnListModel columnList;
     private final SelectModel selectModel;
     private final StatementConfiguration statementConfiguration;
 
@@ -51,11 +51,13 @@ public Optional<InsertColumnListModel> columnList() {
         return Optional.ofNullable(columnList);
     }
 
-    @NotNull
+    public StatementConfiguration statementConfiguration() {
+        return statementConfiguration;
+    }
+
     public InsertSelectStatementProvider render(RenderingStrategy renderingStrategy) {
         return InsertSelectRenderer.withInsertSelectModel(this)
                 .withRenderingStrategy(renderingStrategy)
-                .withStatementConfiguration(statementConfiguration)
                 .build()
                 .render();
     }
@@ -65,17 +67,17 @@ public static Builder withTable(SqlTable table) {
     }
 
     public static class Builder {
-        private SqlTable table;
-        private InsertColumnListModel columnList;
-        private SelectModel selectModel;
-        private StatementConfiguration statementConfiguration;
+        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;
         }
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 1b8feaca3..8e5f30e49 100644
--- a/src/main/java/org/mybatis/dynamic/sql/insert/MultiRowInsertDSL.java
+++ b/src/main/java/org/mybatis/dynamic/sql/insert/MultiRowInsertDSL.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -20,7 +20,6 @@
 import java.util.List;
 import java.util.Objects;
 
-import org.jetbrains.annotations.NotNull;
 import org.mybatis.dynamic.sql.SqlColumn;
 import org.mybatis.dynamic.sql.SqlTable;
 import org.mybatis.dynamic.sql.util.AbstractColumnMapping;
@@ -47,7 +46,6 @@ public <F> ColumnMappingFinisher<F> map(SqlColumn<F> column) {
         return new ColumnMappingFinisher<>(column);
     }
 
-    @NotNull
     @Override
     public MultiRowInsertModel<T> build() {
         return MultiRowInsertModel.withRecords(records)
@@ -57,11 +55,11 @@ public MultiRowInsertModel<T> build() {
     }
 
     @SafeVarargs
-    public static <T> IntoGatherer<T> insert(T... records) {
-        return MultiRowInsertDSL.insert(Arrays.asList(records));
+    public static <T> MultiRowInsertDSL.IntoGatherer<T> insert(T... records) {
+        return insert(Arrays.asList(records));
     }
 
-    public static <T> IntoGatherer<T> insert(Collection<T> records) {
+    public static <T> MultiRowInsertDSL.IntoGatherer<T> insert(Collection<T> records) {
         return new IntoGatherer<>(records);
     }
 
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 406c02b7a..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,7 +17,6 @@
 
 import java.util.Collection;
 
-import org.jetbrains.annotations.NotNull;
 import org.mybatis.dynamic.sql.insert.render.MultiRowInsertRenderer;
 import org.mybatis.dynamic.sql.insert.render.MultiRowInsertStatementProvider;
 import org.mybatis.dynamic.sql.render.RenderingStrategy;
@@ -31,7 +30,6 @@ private MultiRowInsertModel(Builder<T> builder) {
         Validator.assertNotEmpty(columnMappings, "ERROR.8"); //$NON-NLS-1$
     }
 
-    @NotNull
     public MultiRowInsertStatementProvider<T> render(RenderingStrategy renderingStrategy) {
         return MultiRowInsertRenderer.withMultiRowInsertModel(this)
                 .withRenderingStrategy(renderingStrategy)
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 2759bb907..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,7 +19,8 @@
 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<T> {
     private final String insertStatement;
@@ -38,7 +39,7 @@ private BatchInsert(Builder<T> builder) {
     public List<InsertStatementProvider<T>> insertStatements() {
         return records.stream()
                 .map(this::toInsertStatement)
-                .collect(Collectors.toList());
+                .toList();
     }
 
     private InsertStatementProvider<T> toInsertStatement(T row) {
@@ -57,7 +58,7 @@ public String getInsertStatementSQL() {
     }
 
     public List<T> getRecords() {
-        return Collections.unmodifiableList(records);
+        return records;
     }
 
     public static <T> Builder<T> withRecords(List<T> records) {
@@ -65,7 +66,7 @@ public static <T> Builder<T> withRecords(List<T> records) {
     }
 
     public static class Builder<T> {
-        private String insertStatement;
+        private @Nullable String insertStatement;
         private final List<T> records = new ArrayList<>();
 
         public Builder<T> withInsertStatement(String insertStatement) {
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 5242b459b..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,6 +17,7 @@
 
 import java.util.Objects;
 
+import org.jspecify.annotations.Nullable;
 import org.mybatis.dynamic.sql.insert.BatchInsertModel;
 import org.mybatis.dynamic.sql.render.RenderingStrategy;
 
@@ -27,7 +28,8 @@ public class BatchInsertRenderer<T> {
 
     private BatchInsertRenderer(Builder<T> builder) {
         model = Objects.requireNonNull(builder.model);
-        visitor = new MultiRowValuePhraseVisitor(builder.renderingStrategy, "row"); //$NON-NLS-1$)
+        visitor = new MultiRowValuePhraseVisitor(Objects.requireNonNull(builder.renderingStrategy),
+                "row"); //$NON-NLS-1$)
     }
 
     public BatchInsert<T> render() {
@@ -47,8 +49,8 @@ public static <T> Builder<T> withBatchInsertModel(BatchInsertModel<T> model) {
     }
 
     public static class Builder<T> {
-        private BatchInsertModel<T> model;
-        private RenderingStrategy renderingStrategy;
+        private @Nullable BatchInsertModel<T> model;
+        private @Nullable RenderingStrategy renderingStrategy;
 
         public Builder<T> withBatchInsertModel(BatchInsertModel<T> model) {
             this.model = model;
diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/DefaultGeneralInsertStatementProvider.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/DefaultGeneralInsertStatementProvider.java
index de1c41266..eaa3d0911 100644
--- a/src/main/java/org/mybatis/dynamic/sql/insert/render/DefaultGeneralInsertStatementProvider.java
+++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/DefaultGeneralInsertStatementProvider.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,14 +19,16 @@
 import java.util.Map;
 import java.util.Objects;
 
+import org.jspecify.annotations.Nullable;
+
 public class DefaultGeneralInsertStatementProvider
         implements GeneralInsertStatementProvider, InsertSelectStatementProvider {
     private final String insertStatement;
-    private final Map<String, Object> parameters = new HashMap<>();
+    private final Map<String, Object> parameters;
 
     private DefaultGeneralInsertStatementProvider(Builder builder) {
         insertStatement = Objects.requireNonNull(builder.insertStatement);
-        parameters.putAll(builder.parameters);
+        parameters = builder.parameters;
     }
 
     @Override
@@ -44,7 +46,7 @@ public static Builder withInsertStatement(String insertStatement) {
     }
 
     public static class Builder {
-        private String insertStatement;
+        private @Nullable String insertStatement;
         private final Map<String, Object> parameters = new HashMap<>();
 
         public Builder withInsertStatement(String insertStatement) {
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 aae15c34e..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,32 +17,18 @@
 
 import java.util.Objects;
 
-import org.jetbrains.annotations.NotNull;
+import org.jspecify.annotations.Nullable;
 
 public class DefaultInsertStatementProvider<T> implements InsertStatementProvider<T> {
     private final String insertStatement;
-    // need to keep both row and record for now so we don't break
-    // old code. The MyBatis reflection utilities don't handle
-    // the case where the attribute name is different from the getter.
-    //
-    // MyBatis Generator version 1.4.1 (March 8, 2022) changed to use "row" instead of "record".
-    // Target March 2023 for removing "record" from MyBatis Dynamic SQL.
-    private final T record;
     private final T row;
 
     private DefaultInsertStatementProvider(Builder<T> builder) {
         insertStatement = Objects.requireNonNull(builder.insertStatement);
         row = Objects.requireNonNull(builder.row);
-        record = row;
     }
 
     @Override
-    public T getRecord() {
-        return record;
-    }
-
-    @Override
-    @NotNull
     public T getRow() {
         return row;
     }
@@ -57,8 +43,8 @@ public static <T> Builder<T> withRow(T row) {
     }
 
     public static class Builder<T> {
-        private String insertStatement;
-        private T row;
+        private @Nullable String insertStatement;
+        private @Nullable T row;
 
         public Builder<T> withInsertStatement(String insertStatement) {
             this.insertStatement = insertStatement;
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 2bb031e60..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -20,6 +20,8 @@
 import java.util.List;
 import java.util.Objects;
 
+import org.jspecify.annotations.Nullable;
+
 public class DefaultMultiRowInsertStatementProvider<T> implements MultiRowInsertStatementProvider<T> {
 
     private final List<T> records;
@@ -42,7 +44,7 @@ public List<T> getRecords() {
 
     public static class Builder<T> {
         private final List<T> records = new ArrayList<>();
-        private String insertStatement;
+        private @Nullable String insertStatement;
 
         public Builder<T> withRecords(List<T> records) {
             this.records.addAll(records);
diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/FieldAndValueAndParameters.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/FieldAndValueAndParameters.java
index 5c13aa9f6..d923020c0 100644
--- a/src/main/java/org/mybatis/dynamic/sql/insert/render/FieldAndValueAndParameters.java
+++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/FieldAndValueAndParameters.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -20,6 +20,8 @@
 import java.util.Objects;
 import java.util.Optional;
 
+import org.jspecify.annotations.Nullable;
+
 public class FieldAndValueAndParameters {
     private final String fieldName;
     private final String valuePhrase;
@@ -48,8 +50,8 @@ public static Builder withFieldName(String fieldName) {
     }
 
     public static class Builder {
-        private String fieldName;
-        private String valuePhrase;
+        private @Nullable String fieldName;
+        private @Nullable String valuePhrase;
         private final Map<String, Object> parameters = new HashMap<>();
 
         public Builder withFieldName(String fieldName) {
@@ -62,7 +64,10 @@ public Builder withValuePhrase(String valuePhrase) {
             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;
         }
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 c6692b2c8..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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
index afc1d4b98..08eddb54f 100644
--- a/src/main/java/org/mybatis/dynamic/sql/insert/render/GeneralInsertRenderer.java
+++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/GeneralInsertRenderer.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,7 +18,7 @@
 import java.util.Objects;
 import java.util.Optional;
 
-import org.mybatis.dynamic.sql.configuration.StatementConfiguration;
+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;
@@ -31,8 +31,9 @@ public class GeneralInsertRenderer {
 
     private GeneralInsertRenderer(Builder builder) {
         model = Objects.requireNonNull(builder.model);
-        RenderingContext renderingContext = RenderingContext.withRenderingStrategy(builder.renderingStrategy)
-                .withStatementConfiguration(builder.statementConfiguration)
+        RenderingContext renderingContext = RenderingContext
+                .withRenderingStrategy(Objects.requireNonNull(builder.renderingStrategy))
+                .withStatementConfiguration(model.statementConfiguration())
                 .build();
         visitor = new GeneralInsertValuePhraseVisitor(renderingContext);
     }
@@ -40,8 +41,7 @@ private GeneralInsertRenderer(Builder builder) {
     public GeneralInsertStatementProvider render() {
         FieldAndValueCollector collector = model.columnMappings()
                 .map(m -> m.accept(visitor))
-                .filter(Optional::isPresent)
-                .map(Optional::get)
+                .flatMap(Optional::stream)
                 .collect(FieldAndValueCollector.collect());
 
         Validator.assertFalse(collector.isEmpty(), "ERROR.9"); //$NON-NLS-1$
@@ -58,9 +58,8 @@ public static Builder withInsertModel(GeneralInsertModel model) {
     }
 
     public static class Builder {
-        private GeneralInsertModel model;
-        private RenderingStrategy renderingStrategy;
-        private StatementConfiguration statementConfiguration;
+        private @Nullable GeneralInsertModel model;
+        private @Nullable RenderingStrategy renderingStrategy;
 
         public Builder withInsertModel(GeneralInsertModel model) {
             this.model = model;
@@ -72,11 +71,6 @@ public Builder withRenderingStrategy(RenderingStrategy renderingStrategy) {
             return this;
         }
 
-        public Builder withStatementConfiguration(StatementConfiguration statementConfiguration) {
-            this.statementConfiguration = statementConfiguration;
-            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
index a3534059a..394a88ac2 100644
--- a/src/main/java/org/mybatis/dynamic/sql/insert/render/GeneralInsertStatementProvider.java
+++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/GeneralInsertStatementProvider.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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
index 7a7bcf25c..914d710cf 100644
--- a/src/main/java/org/mybatis/dynamic/sql/insert/render/GeneralInsertValuePhraseVisitor.java
+++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/GeneralInsertValuePhraseVisitor.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,6 +18,7 @@
 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;
@@ -74,7 +75,7 @@ public <T> Optional<FieldAndValueAndParameters> visit(ValueWhenPresentMapping<T>
     }
 
     private Optional<FieldAndValueAndParameters> buildValueFragment(AbstractColumnMapping mapping,
-            Object value) {
+            @Nullable Object value) {
         return buildFragment(mapping, value);
     }
 
@@ -84,7 +85,7 @@ private Optional<FieldAndValueAndParameters> buildNullFragment(AbstractColumnMap
                 .buildOptional();
     }
 
-    private Optional<FieldAndValueAndParameters> buildFragment(AbstractColumnMapping mapping, Object value) {
+    private Optional<FieldAndValueAndParameters> buildFragment(AbstractColumnMapping mapping, @Nullable Object value) {
         RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo(mapping.column());
 
         return FieldAndValueAndParameters.withFieldName(mapping.columnName())
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 431b3df8f..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,6 +18,7 @@
 import java.util.Objects;
 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.Validator;
@@ -29,14 +30,13 @@ public class InsertRenderer<T> {
 
     private InsertRenderer(Builder<T> builder) {
         model = Objects.requireNonNull(builder.model);
-        visitor = new ValuePhraseVisitor(builder.renderingStrategy);
+        visitor = new ValuePhraseVisitor(Objects.requireNonNull(builder.renderingStrategy));
     }
 
     public InsertStatementProvider<T> render() {
         FieldAndValueCollector collector = model.columnMappings()
                 .map(m -> m.accept(visitor))
-                .filter(Optional::isPresent)
-                .map(Optional::get)
+                .flatMap(Optional::stream)
                 .collect(FieldAndValueCollector.collect());
 
         Validator.assertFalse(collector.isEmpty(), "ERROR.10"); //$NON-NLS-1$
@@ -53,8 +53,8 @@ public static <T> Builder<T> withInsertModel(InsertModel<T> model) {
     }
 
     public static class Builder<T> {
-        private InsertModel<T> model;
-        private RenderingStrategy renderingStrategy;
+        private @Nullable InsertModel<T> model;
+        private @Nullable RenderingStrategy renderingStrategy;
 
         public Builder<T> withInsertModel(InsertModel<T> model) {
             this.model = model;
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
index 31bbf02ed..de529a2b4 100644
--- a/src/main/java/org/mybatis/dynamic/sql/insert/render/InsertRenderingUtilities.java
+++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/InsertRenderingUtilities.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -31,6 +31,6 @@ public static String calculateInsertStatement(SqlTable table, FieldAndValueColle
     }
 
     public static String calculateInsertStatementStart(SqlTable table) {
-        return "insert into " + table.tableNameAtRuntime(); //$NON-NLS-1$
+        return "insert into " + table.tableName(); //$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 16afe6767..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,20 +15,19 @@
  */
 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.configuration.StatementConfiguration;
 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.util.StringUtilities;
+import org.mybatis.dynamic.sql.select.render.SubQueryRenderer;
+import org.mybatis.dynamic.sql.util.FragmentAndParameters;
 
 public class InsertSelectRenderer {
 
@@ -37,35 +36,35 @@ public class InsertSelectRenderer {
 
     private InsertSelectRenderer(Builder builder) {
         model = Objects.requireNonNull(builder.model);
-        renderingContext = RenderingContext.withRenderingStrategy(builder.renderingStrategy)
-                .withStatementConfiguration(builder.statementConfiguration)
+        renderingContext = RenderingContext.withRenderingStrategy(Objects.requireNonNull(builder.renderingStrategy))
+                .withStatementConfiguration(model.statementConfiguration())
                 .build();
     }
 
     public InsertSelectStatementProvider render() {
-        SelectStatementProvider selectStatement = model.selectModel().render(renderingContext);
-
         String statementStart = InsertRenderingUtilities.calculateInsertStatementStart(model.table());
-        Optional<String> columnsPhrase = calculateColumnsPhrase();
-        String renderedSelectStatement = selectStatement.getSelectStatement();
+        String columnsPhrase = calculateColumnsPhrase();
+        String prefix = statementStart + spaceAfter(columnsPhrase);
 
-        String insertStatement = statementStart
-                + columnsPhrase.map(StringUtilities::spaceBefore).orElse("") //$NON-NLS-1$
-                + spaceBefore(renderedSelectStatement);
+        FragmentAndParameters fragmentAndParameters = SubQueryRenderer.withSelectModel(model.selectModel())
+                .withRenderingContext(renderingContext)
+                .withPrefix(prefix)
+                .build()
+                .render();
 
-        return DefaultGeneralInsertStatementProvider.withInsertStatement(insertStatement)
-                .withParameters(selectStatement.getParameters())
+        return DefaultGeneralInsertStatementProvider.withInsertStatement(fragmentAndParameters.fragment())
+                .withParameters(fragmentAndParameters.parameters())
                 .build();
     }
 
-    private Optional<String> 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.columns()
                 .map(SqlColumn::name)
-                .collect(Collectors.joining(", ", "(", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+                .collect(Collectors.joining(", ", " (", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
     }
 
     public static Builder withInsertSelectModel(InsertSelectModel model) {
@@ -73,9 +72,8 @@ public static Builder withInsertSelectModel(InsertSelectModel model) {
     }
 
     public static class Builder {
-        private InsertSelectModel model;
-        private RenderingStrategy renderingStrategy;
-        private StatementConfiguration statementConfiguration;
+        private @Nullable InsertSelectModel model;
+        private @Nullable RenderingStrategy renderingStrategy;
 
         public Builder withInsertSelectModel(InsertSelectModel model) {
             this.model = model;
@@ -87,11 +85,6 @@ public Builder withRenderingStrategy(RenderingStrategy renderingStrategy) {
             return this;
         }
 
-        public Builder withStatementConfiguration(StatementConfiguration statementConfiguration) {
-            this.statementConfiguration = statementConfiguration;
-            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 bb25ab835..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 6658320f9..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,25 +15,12 @@
  */
 package org.mybatis.dynamic.sql.insert.render;
 
-import org.jetbrains.annotations.NotNull;
-
 public interface InsertStatementProvider<T> {
-    /**
-     * Return the row associated with this insert statement.
-     *
-     * @return the row associated with this insert statement.
-     *
-     * @deprecated in favor of {@link InsertStatementProvider#getRow()}
-     */
-    @Deprecated
-    T getRecord();
-
     /**
      * Return the row associated with this insert statement.
      *
      * @return the row associated with this insert statement.
      */
-    @NotNull
     T getRow();
 
     /**
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 10479ec4b..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,6 +19,7 @@
 
 import java.util.Objects;
 
+import org.jspecify.annotations.Nullable;
 import org.mybatis.dynamic.sql.insert.MultiRowInsertModel;
 import org.mybatis.dynamic.sql.render.RenderingStrategy;
 
@@ -30,7 +31,8 @@ public class MultiRowInsertRenderer<T> {
     private MultiRowInsertRenderer(Builder<T> builder) {
         model = Objects.requireNonNull(builder.model);
         // the prefix is a generic format that will be resolved below with String.format(...)
-        visitor = new MultiRowValuePhraseVisitor(builder.renderingStrategy, "records[%s]"); //$NON-NLS-1$
+        visitor = new MultiRowValuePhraseVisitor(Objects.requireNonNull(builder.renderingStrategy),
+                "records[%s]"); //$NON-NLS-1$
     }
 
     public MultiRowInsertStatementProvider<T> render() {
@@ -58,8 +60,8 @@ public static <T> Builder<T> withMultiRowInsertModel(MultiRowInsertModel<T> mode
     }
 
     public static class Builder<T> {
-        private MultiRowInsertModel<T> model;
-        private RenderingStrategy renderingStrategy;
+        private @Nullable MultiRowInsertModel<T> model;
+        private @Nullable RenderingStrategy renderingStrategy;
 
         public Builder<T> withMultiRowInsertModel(MultiRowInsertModel<T> model) {
             this.model = model;
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 6b520d617..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 9a9bbc3ce..ba2d2ffa5 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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,8 +15,6 @@
  */
 package org.mybatis.dynamic.sql.insert.render;
 
-import java.util.Objects;
-
 import org.mybatis.dynamic.sql.SqlColumn;
 import org.mybatis.dynamic.sql.render.RenderingStrategy;
 import org.mybatis.dynamic.sql.util.ConstantMapping;
@@ -32,8 +30,8 @@ public class MultiRowValuePhraseVisitor extends MultiRowInsertMappingVisitor<Fie
     protected final String prefix;
 
     protected MultiRowValuePhraseVisitor(RenderingStrategy renderingStrategy, String prefix) {
-        this.renderingStrategy = Objects.requireNonNull(renderingStrategy);
-        this.prefix = Objects.requireNonNull(prefix);
+        this.renderingStrategy = renderingStrategy;
+        this.prefix = prefix;
     }
 
     @Override
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 f11909c10..dccf0f14a 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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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
index 70c5287c7..1cb388f85 100644
--- a/src/main/java/org/mybatis/dynamic/sql/render/ExplicitTableAliasCalculator.java
+++ b/src/main/java/org/mybatis/dynamic/sql/render/ExplicitTableAliasCalculator.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 59948b03f..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -38,7 +38,7 @@ public Optional<String> aliasForColumn(SqlTable table) {
         if (alias.isPresent()) {
             return alias;
         } else {
-            return Optional.of(table.tableNameAtRuntime());
+            return Optional.of(table.tableName());
         }
     }
 
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 d104b1186..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/main/java/org/mybatis/dynamic/sql/render/RenderedParameterInfo.java b/src/main/java/org/mybatis/dynamic/sql/render/RenderedParameterInfo.java
index 909f36c3c..5c8187ef2 100644
--- a/src/main/java/org/mybatis/dynamic/sql/render/RenderedParameterInfo.java
+++ b/src/main/java/org/mybatis/dynamic/sql/render/RenderedParameterInfo.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,20 +17,9 @@
 
 import java.util.Objects;
 
-public class RenderedParameterInfo {
-    private final String parameterMapKey;
-    private final String renderedPlaceHolder;
-
+public record RenderedParameterInfo(String parameterMapKey, String renderedPlaceHolder) {
     public RenderedParameterInfo(String parameterMapKey, String renderedPlaceHolder) {
         this.parameterMapKey = Objects.requireNonNull(parameterMapKey);
         this.renderedPlaceHolder = Objects.requireNonNull(renderedPlaceHolder);
     }
-
-    public String parameterMapKey() {
-        return parameterMapKey;
-    }
-
-    public String renderedPlaceHolder() {
-        return 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
index a23c8d07a..4e1457067 100644
--- a/src/main/java/org/mybatis/dynamic/sql/render/RenderingContext.java
+++ b/src/main/java/org/mybatis/dynamic/sql/render/RenderingContext.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -20,6 +20,7 @@
 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;
@@ -37,7 +38,7 @@ public class RenderingContext {
     private final RenderingStrategy renderingStrategy;
     private final AtomicInteger sequence;
     private final TableAliasCalculator tableAliasCalculator;
-    private final String configuredParameterName;
+    private final @Nullable String configuredParameterName;
     private final String calculatedParameterName;
     private final StatementConfiguration statementConfiguration;
 
@@ -53,27 +54,31 @@ private RenderingContext(Builder builder) {
                 : builder.parameterName + "." + RenderingStrategy.DEFAULT_PARAMETER_PREFIX;  //$NON-NLS-1$
     }
 
-    public TableAliasCalculator tableAliasCalculator() {
-        // this method can be removed when the renderWithTableAlias method is removed from BasicColumn
-        return tableAliasCalculator;
-    }
-
     private String nextMapKey() {
         return renderingStrategy.formatParameterMapKey(sequence);
     }
 
-    private String renderedPlaceHolder(String mapKey) {
-        return renderingStrategy.getFormattedJdbcPlaceholder(calculatedParameterName, mapKey);
-    }
-
     private <T> String renderedPlaceHolder(String mapKey, BindableColumn<T> column) {
         return  column.renderingStrategy().orElse(renderingStrategy)
                 .getFormattedJdbcPlaceholder(column, calculatedParameterName, mapKey);
     }
 
-    public RenderedParameterInfo calculateParameterInfo() {
-        String mapKey = nextMapKey();
-        return new RenderedParameterInfo(mapKey, renderedPlaceHolder(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 <T> RenderedParameterInfo calculateParameterInfo(BindableColumn<T> column) {
@@ -93,8 +98,8 @@ public <T> String aliasedColumnName(SqlColumn<T> column, String explicitAlias) {
 
     public String aliasedTableName(SqlTable table) {
         return tableAliasCalculator.aliasForTable(table)
-                .map(a -> table.tableNameAtRuntime() + spaceBefore(a))
-                .orElseGet(table::tableNameAtRuntime);
+                .map(a -> table.tableName() + spaceBefore(a))
+                .orElseGet(table::tableName);
     }
 
     public boolean isNonRenderingClauseAllowed() {
@@ -130,11 +135,11 @@ public static Builder withRenderingStrategy(RenderingStrategy renderingStrategy)
     }
 
     public static class Builder {
-        private RenderingStrategy renderingStrategy;
-        private AtomicInteger sequence;
-        private TableAliasCalculator tableAliasCalculator = TableAliasCalculator.empty();
-        private String parameterName;
-        private StatementConfiguration statementConfiguration;
+        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;
@@ -151,7 +156,7 @@ public Builder withTableAliasCalculator(TableAliasCalculator tableAliasCalculato
             return this;
         }
 
-        public Builder withParameterName(String parameterName) {
+        public Builder withParameterName(@Nullable String parameterName) {
             this.parameterName = parameterName;
             return 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 4b7837a46..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 70b369c73..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -36,10 +36,55 @@
 public abstract class RenderingStrategy {
     public static final String DEFAULT_PARAMETER_PREFIX = "parameters"; //$NON-NLS-1$
 
+    /**
+     * 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
+     */
     public String formatParameterMapKey(AtomicInteger sequence) {
         return "p" + sequence.getAndIncrement(); //$NON-NLS-1$
     }
 
+    /**
+     * Return a parameter map key intended as a parameter for a fetch first query.
+     *
+     * <p>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 formatParameterMapKeyForFetchFirstRows(AtomicInteger sequence) {
+        return formatParameterMapKey(sequence);
+    }
+
+    /**
+     * Return a parameter map key intended as a parameter for a limit query.
+     *
+     * <p>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.
+     *
+     * <p>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.
      *
@@ -78,6 +123,23 @@ public String formatParameterMapKey(AtomicInteger sequence) {
      */
     public abstract String getFormattedJdbcPlaceholder(String prefix, String parameterName);
 
+    /**
+     * This method generates a binding for a parameter to a placeholder in a generated SQL statement.
+     *
+     * <p>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.
      *
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 e11ead4e2..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 faf7fc9a4..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/main/java/org/mybatis/dynamic/sql/render/TableAliasCalculatorWithParent.java b/src/main/java/org/mybatis/dynamic/sql/render/TableAliasCalculatorWithParent.java
index 3aa75e98b..e843751d3 100644
--- a/src/main/java/org/mybatis/dynamic/sql/render/TableAliasCalculatorWithParent.java
+++ b/src/main/java/org/mybatis/dynamic/sql/render/TableAliasCalculatorWithParent.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,6 +18,7 @@
 import java.util.Objects;
 import java.util.Optional;
 
+import org.jspecify.annotations.Nullable;
 import org.mybatis.dynamic.sql.SqlTable;
 
 public class TableAliasCalculatorWithParent implements TableAliasCalculator {
@@ -48,8 +49,8 @@ public Optional<String> aliasForTable(SqlTable table) {
     }
 
     public static class Builder {
-        private TableAliasCalculator parent;
-        private TableAliasCalculator child;
+        private @Nullable TableAliasCalculator parent;
+        private @Nullable TableAliasCalculator child;
 
         public Builder withParent(TableAliasCalculator parent) {
             this.parent = parent;
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
index 2a668c72c..872b964eb 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/AbstractHavingFinisher.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/AbstractHavingFinisher.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,6 +17,7 @@
 
 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;
@@ -27,7 +28,7 @@ void initialize(SqlCriterion sqlCriterion) {
         setInitialCriterion(sqlCriterion, StatementType.HAVING);
     }
 
-    void initialize(SqlCriterion sqlCriterion, List<AndOrCriteriaGroup> subCriteria) {
+    void initialize(@Nullable SqlCriterion sqlCriterion, List<AndOrCriteriaGroup> subCriteria) {
         setInitialCriterion(sqlCriterion, StatementType.HAVING);
         super.subCriteria.addAll(subCriteria);
     }
diff --git a/src/main/java/org/mybatis/dynamic/sql/select/AbstractHavingStarter.java b/src/main/java/org/mybatis/dynamic/sql/select/AbstractHavingStarter.java
index 4eccfc2f6..1090fb064 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/AbstractHavingStarter.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/AbstractHavingStarter.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -22,17 +22,17 @@
 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.VisitableCondition;
 
-public abstract class AbstractHavingStarter<F extends AbstractHavingFinisher<?>> {
+public interface AbstractHavingStarter<F extends AbstractHavingFinisher<?>> {
 
-    public <T> F having(BindableColumn<T> column, VisitableCondition<T> condition,
+    default <T> F having(BindableColumn<T> column, RenderableCondition<T> condition,
                         AndOrCriteriaGroup... subCriteria) {
         return having(column, condition, Arrays.asList(subCriteria));
     }
 
-    public <T> F having(BindableColumn<T> column, VisitableCondition<T> condition,
+    default <T> F having(BindableColumn<T> column, RenderableCondition<T> condition,
                         List<AndOrCriteriaGroup> subCriteria) {
         SqlCriterion sqlCriterion = ColumnAndConditionCriterion.withColumn(column)
                 .withCondition(condition)
@@ -42,11 +42,11 @@ public <T> F having(BindableColumn<T> column, VisitableCondition<T> condition,
         return initialize(sqlCriterion);
     }
 
-    public F having(SqlCriterion initialCriterion, AndOrCriteriaGroup... subCriteria) {
+    default F having(SqlCriterion initialCriterion, AndOrCriteriaGroup... subCriteria) {
         return having(initialCriterion, Arrays.asList(subCriteria));
     }
 
-    public F having(SqlCriterion initialCriterion, List<AndOrCriteriaGroup> subCriteria) {
+    default F having(SqlCriterion initialCriterion, List<AndOrCriteriaGroup> subCriteria) {
         SqlCriterion sqlCriterion = new CriteriaGroup.Builder()
                 .withInitialCriterion(initialCriterion)
                 .withSubCriteria(subCriteria)
@@ -55,9 +55,9 @@ public F having(SqlCriterion initialCriterion, List<AndOrCriteriaGroup> subCrite
         return initialize(sqlCriterion);
     }
 
-    protected abstract F having();
+    F having();
 
-    public F applyHaving(HavingApplier havingApplier) {
+    default F applyHaving(HavingApplier havingApplier) {
         F finisher = having();
         havingApplier.accept(finisher);
         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 d800a02ff..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -23,12 +23,14 @@
 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.TableExpression;
 import org.mybatis.dynamic.sql.exception.DuplicateTableAliasException;
-import org.mybatis.dynamic.sql.select.join.JoinCriterion;
 import org.mybatis.dynamic.sql.select.join.JoinModel;
 import org.mybatis.dynamic.sql.select.join.JoinSpecification;
 import org.mybatis.dynamic.sql.select.join.JoinType;
@@ -38,9 +40,9 @@
 
 public abstract class AbstractQueryExpressionDSL<W extends AbstractWhereFinisher<?>,
             T extends AbstractQueryExpressionDSL<W, T>>
-        extends AbstractWhereStarter<W, T> {
+        implements AbstractWhereStarter<W, T> {
 
-    private final List<JoinSpecification.Builder> joinSpecificationBuilders = new ArrayList<>();
+    private final List<Supplier<JoinSpecification>> joinSpecificationSuppliers = new ArrayList<>();
     private final Map<SqlTable, String> tableAliases = new HashMap<>();
     private final TableExpression table;
 
@@ -52,151 +54,151 @@ 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) {
+    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<JoinCriterion<?>> andJoinCriteria) {
-        addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.INNER, andJoinCriteria);
+    public T join(SqlTable joinTable, @Nullable SqlCriterion onJoinCriterion,
+            List<AndOrCriteriaGroup> andJoinCriteria) {
+        addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.INNER, andJoinCriteria);
         return getThis();
     }
 
-    public T join(SqlTable joinTable, String tableAlias, JoinCriterion<?> onJoinCriterion,
-            List<JoinCriterion<?>> andJoinCriteria) {
+    public T join(SqlTable joinTable, String tableAlias, @Nullable SqlCriterion onJoinCriterion,
+            List<AndOrCriteriaGroup> andJoinCriteria) {
         addTableAlias(joinTable, tableAlias);
         return join(joinTable, onJoinCriterion, andJoinCriteria);
     }
 
-    public T join(Buildable<SelectModel> subQuery, String tableAlias, JoinCriterion<?> onJoinCriterion,
-                  List<JoinCriterion<?>> andJoinCriteria) {
-        addJoinSpecificationBuilder(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.INNER,
+    public T join(Buildable<SelectModel> subQuery, @Nullable String tableAlias, @Nullable SqlCriterion onJoinCriterion,
+                  List<AndOrCriteriaGroup> andJoinCriteria) {
+        addJoinSpecificationSupplier(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.INNER,
                 andJoinCriteria);
         return getThis();
     }
 
-    public T leftJoin(SqlTable joinTable, JoinCriterion<?> onJoinCriterion,
-            JoinCriterion<?>... andJoinCriteria) {
-        addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.LEFT, Arrays.asList(andJoinCriteria));
+    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, JoinCriterion<?> onJoinCriterion,
-            JoinCriterion<?>... andJoinCriteria) {
+    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<JoinCriterion<?>> andJoinCriteria) {
-        addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.LEFT, andJoinCriteria);
+    public T leftJoin(SqlTable joinTable, @Nullable SqlCriterion onJoinCriterion,
+            List<AndOrCriteriaGroup> andJoinCriteria) {
+        addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.LEFT, andJoinCriteria);
         return getThis();
     }
 
-    public T leftJoin(SqlTable joinTable, String tableAlias, JoinCriterion<?> onJoinCriterion,
-            List<JoinCriterion<?>> andJoinCriteria) {
+    public T leftJoin(SqlTable joinTable, String tableAlias, @Nullable SqlCriterion onJoinCriterion,
+            List<AndOrCriteriaGroup> andJoinCriteria) {
         addTableAlias(joinTable, tableAlias);
         return leftJoin(joinTable, onJoinCriterion, andJoinCriteria);
     }
 
-    public T leftJoin(Buildable<SelectModel> subQuery, String tableAlias, JoinCriterion<?> onJoinCriterion,
-                      List<JoinCriterion<?>> andJoinCriteria) {
-        addJoinSpecificationBuilder(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.LEFT,
+    public T leftJoin(Buildable<SelectModel> subQuery, @Nullable String tableAlias,
+                      @Nullable SqlCriterion onJoinCriterion, List<AndOrCriteriaGroup> andJoinCriteria) {
+        addJoinSpecificationSupplier(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.LEFT,
                 andJoinCriteria);
         return getThis();
     }
 
-    public T rightJoin(SqlTable joinTable, JoinCriterion<?> onJoinCriterion,
-            JoinCriterion<?>... andJoinCriteria) {
-        addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.RIGHT, Arrays.asList(andJoinCriteria));
+    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) {
+    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<JoinCriterion<?>> andJoinCriteria) {
-        addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.RIGHT, andJoinCriteria);
+    public T rightJoin(SqlTable joinTable, @Nullable SqlCriterion onJoinCriterion,
+            List<AndOrCriteriaGroup> andJoinCriteria) {
+        addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.RIGHT, andJoinCriteria);
         return getThis();
     }
 
-    public T rightJoin(SqlTable joinTable, String tableAlias, JoinCriterion<?> onJoinCriterion,
-            List<JoinCriterion<?>> andJoinCriteria) {
+    public T rightJoin(SqlTable joinTable, String tableAlias, @Nullable SqlCriterion onJoinCriterion,
+            List<AndOrCriteriaGroup> andJoinCriteria) {
         addTableAlias(joinTable, tableAlias);
         return rightJoin(joinTable, onJoinCriterion, andJoinCriteria);
     }
 
-    public T rightJoin(Buildable<SelectModel> subQuery, String tableAlias, JoinCriterion<?> onJoinCriterion,
-                      List<JoinCriterion<?>> andJoinCriteria) {
-        addJoinSpecificationBuilder(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.RIGHT,
+    public T rightJoin(Buildable<SelectModel> subQuery, @Nullable String tableAlias,
+                       @Nullable SqlCriterion onJoinCriterion, List<AndOrCriteriaGroup> andJoinCriteria) {
+        addJoinSpecificationSupplier(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.RIGHT,
                 andJoinCriteria);
         return getThis();
     }
 
-    public T fullJoin(SqlTable joinTable, JoinCriterion<?> onJoinCriterion,
-            JoinCriterion<?>... andJoinCriteria) {
-        addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.FULL, Arrays.asList(andJoinCriteria));
+    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, JoinCriterion<?> onJoinCriterion,
-            JoinCriterion<?>... andJoinCriteria) {
+    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<JoinCriterion<?>> andJoinCriteria) {
-        addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.FULL, andJoinCriteria);
+    public T fullJoin(SqlTable joinTable, @Nullable SqlCriterion onJoinCriterion,
+            List<AndOrCriteriaGroup> andJoinCriteria) {
+        addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.FULL, andJoinCriteria);
         return getThis();
     }
 
-    public T fullJoin(SqlTable joinTable, String tableAlias, JoinCriterion<?> onJoinCriterion,
-            List<JoinCriterion<?>> andJoinCriteria) {
+    public T fullJoin(SqlTable joinTable, String tableAlias, @Nullable SqlCriterion onJoinCriterion,
+            List<AndOrCriteriaGroup> andJoinCriteria) {
         addTableAlias(joinTable, tableAlias);
         return fullJoin(joinTable, onJoinCriterion, andJoinCriteria);
     }
 
-    public T fullJoin(Buildable<SelectModel> subQuery, String tableAlias, JoinCriterion<?> onJoinCriterion,
-                  List<JoinCriterion<?>> andJoinCriteria) {
-        addJoinSpecificationBuilder(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.FULL,
+    public T fullJoin(Buildable<SelectModel> subQuery, @Nullable String tableAlias,
+                      @Nullable SqlCriterion onJoinCriterion, List<AndOrCriteriaGroup> andJoinCriteria) {
+        addJoinSpecificationSupplier(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.FULL,
                 andJoinCriteria);
         return getThis();
     }
 
-    private void addJoinSpecificationBuilder(TableExpression joinTable, JoinCriterion<?> onJoinCriterion,
-            JoinType joinType, List<JoinCriterion<?>> andJoinCriteria) {
-        joinSpecificationBuilders.add(new JoinSpecification.Builder()
+    private void addJoinSpecificationSupplier(TableExpression joinTable, @Nullable SqlCriterion onJoinCriterion,
+                                              JoinType joinType, List<AndOrCriteriaGroup> 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<JoinSpecification> joinSpecificationSupplier) {
+        joinSpecificationSuppliers.add(joinSpecificationSupplier);
     }
 
     protected Optional<JoinModel> 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) {
@@ -217,7 +219,7 @@ protected static SubQuery buildSubQuery(Buildable<SelectModel> selectModel) {
                 .build();
     }
 
-    protected static SubQuery buildSubQuery(Buildable<SelectModel> selectModel, String alias) {
+    protected static SubQuery buildSubQuery(Buildable<SelectModel> selectModel, @Nullable String alias) {
         return new SubQuery.Builder()
                 .withSelectModel(selectModel.build())
                 .withAlias(alias)
diff --git a/src/main/java/org/mybatis/dynamic/sql/select/AbstractSelectModel.java b/src/main/java/org/mybatis/dynamic/sql/select/AbstractSelectModel.java
index 49c20789f..51c3d4fe7 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/AbstractSelectModel.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/AbstractSelectModel.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,12 +18,13 @@
 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 OrderByModel orderByModel;
-    private final PagingModel pagingModel;
+    private final @Nullable OrderByModel orderByModel;
+    private final @Nullable PagingModel pagingModel;
     protected final StatementConfiguration statementConfiguration;
 
     protected AbstractSelectModel(AbstractBuilder<?> builder) {
@@ -40,17 +41,21 @@ public Optional<PagingModel> pagingModel() {
         return Optional.ofNullable(pagingModel);
     }
 
+    public StatementConfiguration statementConfiguration() {
+        return statementConfiguration;
+    }
+
     public abstract static class AbstractBuilder<T extends AbstractBuilder<T>> {
-        private OrderByModel orderByModel;
-        private PagingModel pagingModel;
-        private StatementConfiguration statementConfiguration;
+        private @Nullable OrderByModel orderByModel;
+        private @Nullable PagingModel pagingModel;
+        private @Nullable StatementConfiguration statementConfiguration;
 
-        public T withOrderByModel(OrderByModel orderByModel) {
+        public T withOrderByModel(@Nullable OrderByModel orderByModel) {
             this.orderByModel = orderByModel;
             return getThis();
         }
 
-        public T withPagingModel(PagingModel pagingModel) {
+        public T withPagingModel(@Nullable PagingModel pagingModel) {
             this.pagingModel = pagingModel;
             return 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
index b8503a971..aa74099b5 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/ColumnSortSpecification.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/ColumnSortSpecification.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,34 +19,31 @@
 
 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 boolean isDescending;
+    private final String descendingPhrase;
 
     public ColumnSortSpecification(String tableAlias, SqlColumn<?> column) {
-        this(tableAlias, column, false);
+        this(tableAlias, column, ""); //$NON-NLS-1$
     }
 
-    private ColumnSortSpecification(String tableAlias, SqlColumn<?> column, boolean isDescending) {
+    private ColumnSortSpecification(String tableAlias, SqlColumn<?> column, String descendingPhrase) {
         this.tableAlias = Objects.requireNonNull(tableAlias);
         this.column = Objects.requireNonNull(column);
-        this.isDescending = isDescending;
+        this.descendingPhrase = descendingPhrase;
     }
 
     @Override
     public SortSpecification descending() {
-        return new ColumnSortSpecification(tableAlias, column, true);
+        return new ColumnSortSpecification(tableAlias, column, " DESC"); //$NON-NLS-1$
     }
 
     @Override
-    public String orderByName() {
-        return tableAlias + "." + column.name(); //$NON-NLS-1$
-    }
-
-    @Override
-    public boolean isDescending() {
-        return isDescending;
+    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 a684bae66..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,13 +19,12 @@
 import java.util.function.Consumer;
 import java.util.function.Function;
 
-import org.jetbrains.annotations.NotNull;
+import org.jspecify.annotations.Nullable;
 import org.mybatis.dynamic.sql.BasicColumn;
 import org.mybatis.dynamic.sql.SqlBuilder;
 import org.mybatis.dynamic.sql.SqlTable;
 import org.mybatis.dynamic.sql.configuration.StatementConfiguration;
 import org.mybatis.dynamic.sql.util.Buildable;
-import org.mybatis.dynamic.sql.util.Utilities;
 import org.mybatis.dynamic.sql.where.AbstractWhereFinisher;
 import org.mybatis.dynamic.sql.where.EmbeddedWhereModel;
 
@@ -42,7 +41,7 @@ public class CountDSL<R> extends AbstractQueryExpressionDSL<CountDSL<R>.CountWhe
         implements Buildable<R> {
 
     private final Function<SelectModel, R> adapterFunction;
-    private CountWhereBuilder whereBuilder;
+    private @Nullable CountWhereBuilder whereBuilder;
     private final BasicColumn countColumn;
     private final StatementConfiguration statementConfiguration = new StatementConfiguration();
 
@@ -54,11 +53,10 @@ private CountDSL(BasicColumn countColumn, SqlTable table, Function<SelectModel,
 
     @Override
     public CountWhereBuilder where() {
-        whereBuilder = Utilities.buildIfNecessary(whereBuilder, CountWhereBuilder::new);
+        whereBuilder = Objects.requireNonNullElseGet(whereBuilder, CountWhereBuilder::new);
         return whereBuilder;
     }
 
-    @NotNull
     @Override
     public R build() {
         return adapterFunction.apply(buildModel());
@@ -134,7 +132,6 @@ private CountWhereBuilder() {
             super(CountDSL.this);
         }
 
-        @NotNull
         @Override
         public R build() {
             return CountDSL.this.build();
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 ac93878fa..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 8123bb4b4..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/main/java/org/mybatis/dynamic/sql/select/HavingApplier.java b/src/main/java/org/mybatis/dynamic/sql/select/HavingApplier.java
index 24ce28a28..d0a61a7fb 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/HavingApplier.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/HavingApplier.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/main/java/org/mybatis/dynamic/sql/select/HavingDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/HavingDSL.java
index 648ddda16..58ca01184 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/HavingDSL.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/HavingDSL.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,14 +15,13 @@
  */
 package org.mybatis.dynamic.sql.select;
 
-import org.jetbrains.annotations.NotNull;
 import org.mybatis.dynamic.sql.util.Buildable;
 
-public class HavingDSL extends AbstractHavingStarter<HavingDSL.StandaloneHavingFinisher> {
+public class HavingDSL implements AbstractHavingStarter<HavingDSL.StandaloneHavingFinisher> {
     private final StandaloneHavingFinisher havingFinisher = new StandaloneHavingFinisher();
 
     @Override
-    protected StandaloneHavingFinisher having() {
+    public StandaloneHavingFinisher having() {
         return havingFinisher;
     }
 
@@ -36,7 +35,6 @@ protected StandaloneHavingFinisher getThis() {
             return this;
         }
 
-        @NotNull
         @Override
         public HavingModel build() {
             return buildModel();
diff --git a/src/main/java/org/mybatis/dynamic/sql/select/HavingModel.java b/src/main/java/org/mybatis/dynamic/sql/select/HavingModel.java
index 5f773cf3b..5f8cd38c8 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/HavingModel.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/HavingModel.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/main/java/org/mybatis/dynamic/sql/select/MultiSelectDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/MultiSelectDSL.java
index ad0f2aba3..124fe095b 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/MultiSelectDSL.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/MultiSelectDSL.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -22,7 +22,7 @@
 import java.util.Optional;
 import java.util.function.Consumer;
 
-import org.jetbrains.annotations.NotNull;
+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;
@@ -32,10 +32,10 @@
 public class MultiSelectDSL implements Buildable<MultiSelectModel>, ConfigurableStatement<MultiSelectDSL> {
     private final List<UnionQuery> unionQueries = new ArrayList<>();
     private final SelectModel initialSelect;
-    private OrderByModel orderByModel;
-    private Long limit;
-    private Long offset;
-    private Long fetchFirstRows;
+    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<SelectModel> builder) {
@@ -61,22 +61,33 @@ public MultiSelectDSL orderBy(Collection<? extends SortSpecification> columns) {
         return this;
     }
 
-    public LimitFinisher limit(long limit) {
+    public MultiSelectDSL.LimitFinisher limit(long limit) {
+        return limitWhenPresent(limit);
+    }
+
+    public MultiSelectDSL.LimitFinisher limitWhenPresent(@Nullable Long limit) {
         this.limit = limit;
         return new LimitFinisher();
     }
 
-    public OffsetFirstFinisher offset(long offset) {
+    public MultiSelectDSL.OffsetFirstFinisher offset(long offset) {
+        return offsetWhenPresent(offset);
+    }
+
+    public MultiSelectDSL.OffsetFirstFinisher offsetWhenPresent(@Nullable Long offset) {
         this.offset = offset;
         return new OffsetFirstFinisher();
     }
 
-    public FetchFirstFinisher fetchFirst(long fetchFirstRows) {
+    public MultiSelectDSL.FetchFirstFinisher fetchFirst(long fetchFirstRows) {
+        return fetchFirstWhenPresent(fetchFirstRows);
+    }
+
+    public MultiSelectDSL.FetchFirstFinisher fetchFirstWhenPresent(@Nullable Long fetchFirstRows) {
         this.fetchFirstRows = fetchFirstRows;
         return new FetchFirstFinisher();
     }
 
-    @NotNull
     @Override
     public MultiSelectModel build() {
         return new MultiSelectModel.Builder()
@@ -102,34 +113,32 @@ public MultiSelectDSL configureStatement(Consumer<StatementConfiguration> consum
         return this;
     }
 
-    public class LimitFinisher implements Buildable<MultiSelectModel> {
-        public OffsetFinisher offset(long offset) {
-            MultiSelectDSL.this.offset(offset);
-            return new OffsetFinisher();
+    public class OffsetFirstFinisher implements Buildable<MultiSelectModel> {
+        public FetchFirstFinisher fetchFirst(long fetchFirstRows) {
+            return fetchFirstWhenPresent(fetchFirstRows);
         }
 
-        @NotNull
-        @Override
-        public MultiSelectModel build() {
-            return MultiSelectDSL.this.build();
+        public FetchFirstFinisher fetchFirstWhenPresent(@Nullable Long fetchFirstRows) {
+            MultiSelectDSL.this.fetchFirstRows = fetchFirstRows;
+            return new FetchFirstFinisher();
         }
-    }
 
-    public class OffsetFinisher implements Buildable<MultiSelectModel> {
-        @NotNull
         @Override
         public MultiSelectModel build() {
             return MultiSelectDSL.this.build();
         }
     }
 
-    public class OffsetFirstFinisher implements Buildable<MultiSelectModel> {
-        public FetchFirstFinisher fetchFirst(long fetchFirstRows) {
-            MultiSelectDSL.this.fetchFirst(fetchFirstRows);
-            return new FetchFirstFinisher();
+    public class LimitFinisher implements Buildable<MultiSelectModel> {
+        public MultiSelectDSL offset(long offset) {
+            return offsetWhenPresent(offset);
+        }
+
+        public MultiSelectDSL offsetWhenPresent(@Nullable Long offset) {
+            MultiSelectDSL.this.offset = offset;
+            return MultiSelectDSL.this;
         }
 
-        @NotNull
         @Override
         public MultiSelectModel build() {
             return MultiSelectDSL.this.build();
@@ -137,16 +146,8 @@ public MultiSelectModel build() {
     }
 
     public class FetchFirstFinisher {
-        public RowsOnlyFinisher rowsOnly() {
-            return new RowsOnlyFinisher();
-        }
-    }
-
-    public class RowsOnlyFinisher implements Buildable<MultiSelectModel> {
-        @NotNull
-        @Override
-        public MultiSelectModel build() {
-            return MultiSelectDSL.this.build();
+        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
index f9a1a388a..94dbe765b 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/MultiSelectModel.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/MultiSelectModel.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -20,7 +20,7 @@
 import java.util.Objects;
 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.MultiSelectRenderer;
 import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
@@ -45,18 +45,15 @@ public Stream<UnionQuery> unionQueries() {
         return unionQueries.stream();
     }
 
-    @NotNull
     public SelectStatementProvider render(RenderingStrategy renderingStrategy) {
-        return new MultiSelectRenderer.Builder()
-                .withMultiSelectModel(this)
+        return MultiSelectRenderer.withMultiSelectModel(this)
                 .withRenderingStrategy(renderingStrategy)
-                .withStatementConfiguration(statementConfiguration)
                 .build()
                 .render();
     }
 
     public static class Builder extends AbstractBuilder<Builder> {
-        private SelectModel initialSelect;
+        private @Nullable SelectModel initialSelect;
         private final List<UnionQuery> unionQueries = new ArrayList<>();
 
         public Builder withInitialSelect(SelectModel initialSelect) {
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 603f40001..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,11 +17,13 @@
 
 import java.util.Optional;
 
+import org.jspecify.annotations.Nullable;
+
 public class PagingModel {
 
-    private final Long limit;
-    private final Long offset;
-    private final Long fetchFirstRows;
+    private final @Nullable Long limit;
+    private final @Nullable Long offset;
+    private final @Nullable Long fetchFirstRows;
 
     private PagingModel(Builder builder) {
         super();
@@ -43,21 +45,21 @@ public Optional<Long> 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;
         }
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 593252979..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -22,35 +22,36 @@
 import java.util.Objects;
 import java.util.function.Consumer;
 
-import org.jetbrains.annotations.NotNull;
+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.SqlTable;
 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.JoinCondition;
-import org.mybatis.dynamic.sql.select.join.JoinCriterion;
 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.util.Utilities;
 import org.mybatis.dynamic.sql.where.AbstractWhereFinisher;
 import org.mybatis.dynamic.sql.where.AbstractWhereStarter;
 import org.mybatis.dynamic.sql.where.EmbeddedWhereModel;
 
 public class QueryExpressionDSL<R>
         extends AbstractQueryExpressionDSL<QueryExpressionDSL<R>.QueryExpressionWhereBuilder, QueryExpressionDSL<R>>
-        implements Buildable<R> {
+        implements Buildable<R>, SelectDSLOperations<R> {
 
-    private final String connector;
+    private final @Nullable String connector;
     private final SelectDSL<R> selectDSL;
     private final boolean isDistinct;
     private final List<BasicColumn> selectList;
-    private QueryExpressionWhereBuilder whereBuilder;
-    private GroupByModel groupByModel;
-    private QueryExpressionHavingBuilder havingBuilder;
+    private @Nullable QueryExpressionWhereBuilder whereBuilder;
+    private @Nullable GroupByModel groupByModel;
+    private @Nullable QueryExpressionHavingBuilder havingBuilder;
 
     protected QueryExpressionDSL(FromGatherer<R> fromGatherer, TableExpression table) {
         super(table);
@@ -68,7 +69,7 @@ protected QueryExpressionDSL(FromGatherer<R> fromGatherer, SqlTable table, Strin
 
     @Override
     public QueryExpressionWhereBuilder where() {
-        whereBuilder = Utilities.buildIfNecessary(whereBuilder, QueryExpressionWhereBuilder::new);
+        whereBuilder = Objects.requireNonNullElseGet(whereBuilder, QueryExpressionWhereBuilder::new);
         return whereBuilder;
     }
 
@@ -84,7 +85,7 @@ public QueryExpressionDSL<R> configureStatement(Consumer<StatementConfiguration>
      * @return The having builder
      */
     protected QueryExpressionHavingBuilder having() {
-        havingBuilder = Utilities.buildIfNecessary(havingBuilder, QueryExpressionHavingBuilder::new);
+        havingBuilder = Objects.requireNonNullElseGet(havingBuilder, QueryExpressionHavingBuilder::new);
         return havingBuilder;
     }
 
@@ -97,7 +98,6 @@ protected void applyHaving(CriteriaGroup criteriaGroup) {
         having().initialize(criteriaGroup);
     }
 
-    @NotNull
     @Override
     public R build() {
         return selectDSL.build();
@@ -194,25 +194,18 @@ protected QueryExpressionModel buildModel() {
                 .build();
     }
 
-    public SelectDSL<R>.LimitFinisher limit(long limit) {
-        return selectDSL.limit(limit);
-    }
-
-    public SelectDSL<R>.OffsetFirstFinisher offset(long offset) {
-        return selectDSL.offset(offset);
-    }
-
-    public SelectDSL<R>.FetchFirstFinisher fetchFirst(long fetchFirstRows) {
-        return selectDSL.fetchFirst(fetchFirstRows);
-    }
-
     @Override
     protected QueryExpressionDSL<R> getThis() {
         return this;
     }
 
+    @Override
+    public SelectDSL<R> getSelectDSL() {
+        return selectDSL;
+    }
+
     public static class FromGatherer<R> {
-        private final String connector;
+        private final @Nullable String connector;
         private final List<BasicColumn> selectList;
         private final SelectDSL<R> selectDSL;
         private final boolean isDistinct;
@@ -241,9 +234,9 @@ public QueryExpressionDSL<R> from(SqlTable table, String tableAlias) {
         }
 
         public static class Builder<R> {
-            private String connector;
+            private @Nullable String connector;
             private final List<BasicColumn> selectList = new ArrayList<>();
-            private SelectDSL<R> selectDSL;
+            private @Nullable SelectDSL<R> selectDSL;
             private boolean isDistinct;
 
             public Builder<R> withConnector(String connector) {
@@ -273,7 +266,7 @@ public FromGatherer<R> build() {
     }
 
     public class QueryExpressionWhereBuilder extends AbstractWhereFinisher<QueryExpressionWhereBuilder>
-            implements Buildable<R> {
+            implements Buildable<R>, SelectDSLOperations<R> {
         private QueryExpressionWhereBuilder() {
             super(QueryExpressionDSL.this);
         }
@@ -302,19 +295,6 @@ public GroupByFinisher groupBy(Collection<? extends BasicColumn> columns) {
             return QueryExpressionDSL.this.groupBy(columns);
         }
 
-        public SelectDSL<R>.LimitFinisher limit(long limit) {
-            return QueryExpressionDSL.this.limit(limit);
-        }
-
-        public SelectDSL<R>.OffsetFirstFinisher offset(long offset) {
-            return QueryExpressionDSL.this.offset(offset);
-        }
-
-        public SelectDSL<R>.FetchFirstFinisher fetchFirst(long fetchFirstRows) {
-            return QueryExpressionDSL.this.fetchFirst(fetchFirstRows);
-        }
-
-        @NotNull
         @Override
         public R build() {
             return QueryExpressionDSL.this.build();
@@ -325,6 +305,11 @@ protected QueryExpressionWhereBuilder getThis() {
             return this;
         }
 
+        @Override
+        public SelectDSL<R> getSelectDSL() {
+            return QueryExpressionDSL.this.getSelectDSL();
+        }
+
         protected EmbeddedWhereModel buildWhereModel() {
             return super.buildModel();
         }
@@ -339,53 +324,60 @@ public JoinSpecificationStarter(TableExpression joinTable, JoinType joinType) {
             this.joinType = joinType;
         }
 
-        public <T> JoinSpecificationFinisher on(BindableColumn<T> joinColumn, JoinCondition<T> joinCondition) {
+        public <T> JoinSpecificationFinisher on(BindableColumn<T> joinColumn, RenderableCondition<T> joinCondition) {
             return new JoinSpecificationFinisher(joinTable, joinColumn, joinCondition, joinType);
         }
 
-        public <T> JoinSpecificationFinisher on(BindableColumn<T> joinColumn, JoinCondition<T> onJoinCondition,
-                JoinCriterion<?>... andJoinCriteria) {
-            return new JoinSpecificationFinisher(joinTable, joinColumn, onJoinCondition, joinType, andJoinCriteria);
+        public <T> JoinSpecificationFinisher on(BindableColumn<T> joinColumn, RenderableCondition<T> onJoinCondition,
+                                                AndOrCriteriaGroup... subCriteria) {
+            return new JoinSpecificationFinisher(joinTable, joinColumn, onJoinCondition, joinType, subCriteria);
         }
     }
 
     public class JoinSpecificationFinisher
-            extends AbstractWhereStarter<QueryExpressionWhereBuilder, JoinSpecificationFinisher>
-            implements Buildable<R> {
-        private final JoinSpecification.Builder joinSpecificationBuilder;
+            extends AbstractBooleanExpressionDSL<JoinSpecificationFinisher>
+            implements AbstractWhereStarter<QueryExpressionWhereBuilder, JoinSpecificationFinisher>, Buildable<R>,
+            SelectDSLOperations<R> {
+
+        private final TableExpression table;
+        private final JoinType joinType;
 
         public <T> JoinSpecificationFinisher(TableExpression table, BindableColumn<T> joinColumn,
-                JoinCondition<T> joinCondition, JoinType joinType) {
-            JoinCriterion<T> joinCriterion = new JoinCriterion.Builder<T>()
-                    .withConnector("on") //$NON-NLS-1$
-                    .withJoinColumn(joinColumn)
-                    .withJoinCondition(joinCondition)
-                    .build();
+                                             RenderableCondition<T> joinCondition, JoinType joinType) {
+            this.table = table;
+            this.joinType = joinType;
+            addJoinSpecificationSupplier(this::buildJoinSpecification);
 
-            joinSpecificationBuilder = JoinSpecification.withJoinTable(table)
-                    .withJoinType(joinType)
-                    .withJoinCriterion(joinCriterion);
+            ColumnAndConditionCriterion<T> criterion = ColumnAndConditionCriterion.withColumn(joinColumn)
+                    .withCondition(joinCondition)
+                    .build();
 
-            addJoinSpecificationBuilder(joinSpecificationBuilder);
+            setInitialCriterion(criterion);
         }
 
         public <T> JoinSpecificationFinisher(TableExpression table, BindableColumn<T> joinColumn,
-                JoinCondition<T> joinCondition, JoinType joinType, JoinCriterion<?>... andJoinCriteria) {
-            JoinCriterion<T> onJoinCriterion = new JoinCriterion.Builder<T>()
-                    .withConnector("on") //$NON-NLS-1$
-                    .withJoinColumn(joinColumn)
-                    .withJoinCondition(joinCondition)
+                                             RenderableCondition<T> joinCondition, JoinType joinType,
+                                             AndOrCriteriaGroup... subCriteria) {
+            this.table = table;
+            this.joinType = joinType;
+            addJoinSpecificationSupplier(this::buildJoinSpecification);
+
+            ColumnAndConditionCriterion<T> criterion = ColumnAndConditionCriterion.withColumn(joinColumn)
+                    .withCondition(joinCondition)
+                    .withSubCriteria(Arrays.asList(subCriteria))
                     .build();
 
-            joinSpecificationBuilder = JoinSpecification.withJoinTable(table)
-                    .withJoinType(joinType)
-                    .withJoinCriterion(onJoinCriterion)
-                    .withJoinCriteria(Arrays.asList(andJoinCriteria));
+            setInitialCriterion(criterion);
+        }
 
-            addJoinSpecificationBuilder(joinSpecificationBuilder);
+        private JoinSpecification buildJoinSpecification() {
+            return JoinSpecification.withJoinTable(table)
+                    .withJoinType(joinType)
+                    .withInitialCriterion(getInitialCriterion())
+                    .withSubCriteria(subCriteria)
+                    .build();
         }
 
-        @NotNull
         @Override
         public R build() {
             return QueryExpressionDSL.this.build();
@@ -402,16 +394,6 @@ public QueryExpressionWhereBuilder where() {
             return QueryExpressionDSL.this.where();
         }
 
-        public <T> JoinSpecificationFinisher and(BindableColumn<T> joinColumn, JoinCondition<T> joinCondition) {
-            JoinCriterion<T> joinCriterion = new JoinCriterion.Builder<T>()
-                    .withConnector("and") //$NON-NLS-1$
-                    .withJoinColumn(joinColumn)
-                    .withJoinCondition(joinCondition)
-                    .build();
-            joinSpecificationBuilder.withJoinCriterion(joinCriterion);
-            return this;
-        }
-
         public JoinSpecificationStarter join(SqlTable joinTable) {
             return QueryExpressionDSL.this.join(joinTable);
         }
@@ -484,20 +466,19 @@ public SelectDSL<R> orderBy(Collection<? extends SortSpecification> columns) {
             return QueryExpressionDSL.this.orderBy(columns);
         }
 
-        public SelectDSL<R>.LimitFinisher limit(long limit) {
-            return QueryExpressionDSL.this.limit(limit);
-        }
-
-        public SelectDSL<R>.OffsetFirstFinisher offset(long offset) {
-            return QueryExpressionDSL.this.offset(offset);
+        @Override
+        protected JoinSpecificationFinisher getThis() {
+            return this;
         }
 
-        public SelectDSL<R>.FetchFirstFinisher fetchFirst(long fetchFirstRows) {
-            return QueryExpressionDSL.this.fetchFirst(fetchFirstRows);
+        @Override
+        public SelectDSL<R> getSelectDSL() {
+            return QueryExpressionDSL.this.getSelectDSL();
         }
     }
 
-    public class GroupByFinisher extends AbstractHavingStarter<QueryExpressionHavingBuilder> implements Buildable<R> {
+    public class GroupByFinisher implements AbstractHavingStarter<QueryExpressionHavingBuilder>,
+            Buildable<R>, SelectDSLOperations<R> {
         public SelectDSL<R> orderBy(SortSpecification... columns) {
             return orderBy(Arrays.asList(columns));
         }
@@ -506,7 +487,6 @@ public SelectDSL<R> orderBy(Collection<? extends SortSpecification> columns) {
             return QueryExpressionDSL.this.orderBy(columns);
         }
 
-        @NotNull
         @Override
         public R build() {
             return QueryExpressionDSL.this.build();
@@ -520,22 +500,15 @@ public UnionBuilder unionAll() {
             return QueryExpressionDSL.this.unionAll();
         }
 
-        public SelectDSL<R>.LimitFinisher limit(long limit) {
-            return QueryExpressionDSL.this.limit(limit);
-        }
-
-        public SelectDSL<R>.OffsetFirstFinisher offset(long offset) {
-            return QueryExpressionDSL.this.offset(offset);
-        }
-
-        public SelectDSL<R>.FetchFirstFinisher fetchFirst(long fetchFirstRows) {
-            return QueryExpressionDSL.this.fetchFirst(fetchFirstRows);
-        }
-
         @Override
         public QueryExpressionHavingBuilder having() {
             return QueryExpressionDSL.this.having();
         }
+
+        @Override
+        public SelectDSL<R> getSelectDSL() {
+            return QueryExpressionDSL.this.getSelectDSL();
+        }
     }
 
     public class UnionBuilder {
@@ -572,19 +545,7 @@ public FromGatherer<R> selectDistinct(List<BasicColumn> selectList) {
     }
 
     public class QueryExpressionHavingBuilder extends AbstractHavingFinisher<QueryExpressionHavingBuilder>
-            implements Buildable<R> {
-
-        public SelectDSL<R>.FetchFirstFinisher fetchFirst(long fetchFirstRows) {
-            return QueryExpressionDSL.this.fetchFirst(fetchFirstRows);
-        }
-
-        public SelectDSL<R>.OffsetFirstFinisher offset(long offset) {
-            return QueryExpressionDSL.this.offset(offset);
-        }
-
-        public SelectDSL<R>.LimitFinisher limit(long limit) {
-            return QueryExpressionDSL.this.limit(limit);
-        }
+            implements Buildable<R>, SelectDSLOperations<R> {
 
         public SelectDSL<R> orderBy(SortSpecification... columns) {
             return orderBy(Arrays.asList(columns));
@@ -602,7 +563,6 @@ public UnionBuilder unionAll() {
             return QueryExpressionDSL.this.unionAll();
         }
 
-        @NotNull
         @Override
         public R build() {
             return QueryExpressionDSL.this.build();
@@ -616,5 +576,10 @@ protected QueryExpressionHavingBuilder getThis() {
         protected HavingModel buildHavingModel() {
             return super.buildModel();
         }
+
+        @Override
+        public SelectDSL<R> 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 f67060aad..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -23,6 +23,7 @@
 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.SqlTable;
 import org.mybatis.dynamic.sql.TableExpression;
@@ -31,15 +32,15 @@
 import org.mybatis.dynamic.sql.where.EmbeddedWhereModel;
 
 public class QueryExpressionModel {
-    private final String connector;
+    private final @Nullable String connector;
     private final boolean isDistinct;
     private final List<BasicColumn> selectList;
     private final TableExpression table;
-    private final JoinModel joinModel;
+    private final @Nullable JoinModel joinModel;
     private final Map<SqlTable, String> tableAliases;
-    private final EmbeddedWhereModel whereModel;
-    private final GroupByModel groupByModel;
-    private final HavingModel havingModel;
+    private final @Nullable EmbeddedWhereModel whereModel;
+    private final @Nullable GroupByModel groupByModel;
+    private final @Nullable HavingModel havingModel;
 
     private QueryExpressionModel(Builder builder) {
         connector = builder.connector;
@@ -95,17 +96,17 @@ public static Builder withSelectList(List<? extends BasicColumn> columnList) {
     }
 
     public static class Builder {
-        private String connector;
+        private @Nullable String connector;
         private boolean isDistinct;
         private final List<BasicColumn> selectList = new ArrayList<>();
-        private TableExpression table;
+        private @Nullable TableExpression table;
         private final Map<SqlTable, String> tableAliases = new HashMap<>();
-        private EmbeddedWhereModel whereModel;
-        private JoinModel joinModel;
-        private GroupByModel groupByModel;
-        private HavingModel havingModel;
+        private @Nullable EmbeddedWhereModel whereModel;
+        private @Nullable JoinModel joinModel;
+        private @Nullable GroupByModel groupByModel;
+        private @Nullable HavingModel havingModel;
 
-        public Builder withConnector(String connector) {
+        public Builder withConnector(@Nullable String connector) {
             this.connector = connector;
             return this;
         }
@@ -135,22 +136,22 @@ public Builder withTableAliases(Map<SqlTable, String> tableAliases) {
             return this;
         }
 
-        public Builder withWhereModel(EmbeddedWhereModel 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(HavingModel havingModel) {
+        public Builder withHavingModel(@Nullable HavingModel havingModel) {
             this.havingModel = havingModel;
             return 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 a509e974b..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -23,9 +23,8 @@
 import java.util.Optional;
 import java.util.function.Consumer;
 import java.util.function.Function;
-import java.util.stream.Collectors;
 
-import org.jetbrains.annotations.NotNull;
+import org.jspecify.annotations.Nullable;
 import org.mybatis.dynamic.sql.BasicColumn;
 import org.mybatis.dynamic.sql.SortSpecification;
 import org.mybatis.dynamic.sql.common.OrderByModel;
@@ -33,6 +32,7 @@
 import org.mybatis.dynamic.sql.select.QueryExpressionDSL.FromGatherer;
 import org.mybatis.dynamic.sql.util.Buildable;
 import org.mybatis.dynamic.sql.util.ConfigurableStatement;
+import org.mybatis.dynamic.sql.util.Validator;
 
 /**
  * Implements a SQL DSL for building select statements.
@@ -46,11 +46,13 @@ public class SelectDSL<R> implements Buildable<R>, ConfigurableStatement<SelectD
 
     private final Function<SelectModel, R> adapterFunction;
     private final List<QueryExpressionDSL<R>> queryExpressions = new ArrayList<>();
-    private OrderByModel orderByModel;
-    private Long limit;
-    private Long offset;
-    private Long fetchFirstRows;
+    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<SelectModel, R> adapterFunction) {
         this.adapterFunction = Objects.requireNonNull(adapterFunction);
@@ -108,34 +110,83 @@ void orderBy(Collection<? extends SortSpecification> columns) {
         orderByModel = OrderByModel.of(columns);
     }
 
-    public LimitFinisher limit(long limit) {
+    public SelectDSL<R>.LimitFinisher limit(long limit) {
+        return limitWhenPresent(limit);
+    }
+
+    public SelectDSL<R>.LimitFinisher limitWhenPresent(@Nullable Long limit) {
         this.limit = limit;
         return new LimitFinisher();
     }
 
-    public OffsetFirstFinisher offset(long offset) {
+    public SelectDSL<R>.OffsetFirstFinisher offset(long offset) {
+        return offsetWhenPresent(offset);
+    }
+
+    public SelectDSL<R>.OffsetFirstFinisher offsetWhenPresent(@Nullable Long offset) {
         this.offset = offset;
         return new OffsetFirstFinisher();
     }
 
-    public FetchFirstFinisher fetchFirst(long fetchFirstRows) {
+    public SelectDSL<R>.FetchFirstFinisher fetchFirst(long fetchFirstRows) {
+        return fetchFirstWhenPresent(fetchFirstRows);
+    }
+
+    public SelectDSL<R>.FetchFirstFinisher fetchFirstWhenPresent(@Nullable Long fetchFirstRows) {
         this.fetchFirstRows = fetchFirstRows;
         return new FetchFirstFinisher();
     }
 
+    public SelectDSL<R> forUpdate() {
+        Validator.assertNull(forClause, "ERROR.48"); //$NON-NLS-1$
+        forClause = "for update"; //$NON-NLS-1$
+        return this;
+    }
+
+    public SelectDSL<R> forNoKeyUpdate() {
+        Validator.assertNull(forClause, "ERROR.48"); //$NON-NLS-1$
+        forClause = "for no key update"; //$NON-NLS-1$
+        return this;
+    }
+
+    public SelectDSL<R> forShare() {
+        Validator.assertNull(forClause, "ERROR.48"); //$NON-NLS-1$
+        forClause = "for share"; //$NON-NLS-1$
+        return this;
+    }
+
+    public SelectDSL<R> forKeyShare() {
+        Validator.assertNull(forClause, "ERROR.48"); //$NON-NLS-1$
+        forClause = "for key share"; //$NON-NLS-1$
+        return this;
+    }
+
+    public SelectDSL<R> skipLocked() {
+        Validator.assertNull(waitClause, "ERROR.49"); //$NON-NLS-1$
+        waitClause = "skip locked"; //$NON-NLS-1$
+        return this;
+    }
+
+    public SelectDSL<R> nowait() {
+        Validator.assertNull(waitClause, "ERROR.49"); //$NON-NLS-1$
+        waitClause = "nowait"; //$NON-NLS-1$
+        return this;
+    }
+
     @Override
     public SelectDSL<R> configureStatement(Consumer<StatementConfiguration> consumer) {
         consumer.accept(statementConfiguration);
         return this;
     }
 
-    @NotNull
     @Override
     public R build() {
         SelectModel selectModel = SelectModel.withQueryExpressions(buildModels())
                 .withOrderByModel(orderByModel)
                 .withPagingModel(buildPagingModel().orElse(null))
                 .withStatementConfiguration(statementConfiguration)
+                .withForClause(forClause)
+                .withWaitClause(waitClause)
                 .build();
         return adapterFunction.apply(selectModel);
     }
@@ -143,7 +194,7 @@ public R build() {
     private List<QueryExpressionModel> buildModels() {
         return queryExpressions.stream()
                 .map(QueryExpressionDSL::buildModel)
-                .collect(Collectors.toList());
+                .toList();
     }
 
     private Optional<PagingModel> buildPagingModel() {
@@ -154,51 +205,51 @@ private Optional<PagingModel> buildPagingModel() {
                 .build();
     }
 
-    public class LimitFinisher implements Buildable<R> {
-        public OffsetFinisher offset(long offset) {
-            SelectDSL.this.offset(offset);
-            return new OffsetFinisher();
+    public class OffsetFirstFinisher implements SelectDSLForAndWaitOperations<R>, Buildable<R> {
+        public FetchFirstFinisher fetchFirst(long fetchFirstRows) {
+            return fetchFirstWhenPresent(fetchFirstRows);
+        }
+
+        public FetchFirstFinisher fetchFirstWhenPresent(@Nullable Long fetchFirstRows) {
+            SelectDSL.this.fetchFirstRows = fetchFirstRows;
+            return new FetchFirstFinisher();
         }
 
-        @NotNull
         @Override
-        public R build() {
-            return SelectDSL.this.build();
+        public SelectDSL<R> getSelectDSL() {
+            return SelectDSL.this;
         }
-    }
 
-    public class OffsetFinisher implements Buildable<R> {
-        @NotNull
         @Override
         public R build() {
             return SelectDSL.this.build();
         }
     }
 
-    public class OffsetFirstFinisher implements Buildable<R> {
-        public FetchFirstFinisher fetchFirst(long fetchFirstRows) {
-            SelectDSL.this.fetchFirst(fetchFirstRows);
-            return new FetchFirstFinisher();
+    public class LimitFinisher implements SelectDSLForAndWaitOperations<R>, Buildable<R> {
+        public SelectDSL<R> offset(long offset) {
+            return offsetWhenPresent(offset);
         }
 
-        @NotNull
-        @Override
-        public R build() {
-            return SelectDSL.this.build();
+        public SelectDSL<R> offsetWhenPresent(@Nullable Long offset) {
+            SelectDSL.this.offset = offset;
+            return SelectDSL.this;
         }
-    }
 
-    public class FetchFirstFinisher {
-        public RowsOnlyFinisher rowsOnly() {
-            return new RowsOnlyFinisher();
+        @Override
+        public SelectDSL<R> getSelectDSL() {
+            return SelectDSL.this;
         }
-    }
 
-    public class RowsOnlyFinisher implements Buildable<R> {
-        @NotNull
         @Override
         public R build() {
             return SelectDSL.this.build();
         }
     }
+
+    public class FetchFirstFinisher {
+        public SelectDSL<R> 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 4252dfc8d..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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<R> {
+    default SelectDSL<R> forUpdate() {
+        return getSelectDSL().forUpdate();
+    }
+
+    default SelectDSL<R> forNoKeyUpdate() {
+        return getSelectDSL().forNoKeyUpdate();
+    }
+
+    default SelectDSL<R> forShare() {
+        return getSelectDSL().forShare();
+    }
+
+    default SelectDSL<R> forKeyShare() {
+        return getSelectDSL().forKeyShare();
+    }
+
+    default SelectDSL<R> skipLocked() {
+        return getSelectDSL().skipLocked();
+    }
+
+    default SelectDSL<R> nowait() {
+        return getSelectDSL().nowait();
+    }
+
+    /**
+     * Gain access to the SelectDSL instance.
+     *
+     * <p>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<R> 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<R> extends SelectDSLForAndWaitOperations<R> {
+    default SelectDSL<R>.LimitFinisher limit(long limit) {
+        return getSelectDSL().limit(limit);
+    }
+
+    default SelectDSL<R>.LimitFinisher limitWhenPresent(@Nullable Long limit) {
+        return getSelectDSL().limitWhenPresent(limit);
+    }
+
+    default SelectDSL<R>.OffsetFirstFinisher offset(long offset) {
+        return getSelectDSL().offset(offset);
+    }
+
+    default SelectDSL<R>.OffsetFirstFinisher offsetWhenPresent(@Nullable Long offset) {
+        return getSelectDSL().offsetWhenPresent(offset);
+    }
+
+    default SelectDSL<R>.FetchFirstFinisher fetchFirst(long fetchFirstRows) {
+        return getSelectDSL().fetchFirst(fetchFirstRows);
+    }
+
+    default SelectDSL<R>.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 57fd06a43..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,10 +18,10 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.stream.Stream;
 
-import org.jetbrains.annotations.NotNull;
-import org.mybatis.dynamic.sql.render.RenderingContext;
+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;
@@ -29,35 +29,32 @@
 
 public class SelectModel extends AbstractSelectModel {
     private final List<QueryExpressionModel> queryExpressions;
+    private final @Nullable String forClause;
+    private final @Nullable String waitClause;
 
     private SelectModel(Builder builder) {
         super(builder);
         queryExpressions = Objects.requireNonNull(builder.queryExpressions);
         Validator.assertNotEmpty(queryExpressions, "ERROR.14"); //$NON-NLS-1$
+        forClause = builder.forClause;
+        waitClause = builder.waitClause;
     }
 
     public Stream<QueryExpressionModel> queryExpressions() {
         return queryExpressions.stream();
     }
 
-    @NotNull
-    public SelectStatementProvider render(RenderingStrategy renderingStrategy) {
-        RenderingContext renderingContext = RenderingContext.withRenderingStrategy(renderingStrategy)
-                .withStatementConfiguration(statementConfiguration)
-                .build();
-        return render(renderingContext);
+    public Optional<String> forClause() {
+        return Optional.ofNullable(forClause);
+    }
+
+    public Optional<String> waitClause() {
+        return Optional.ofNullable(waitClause);
     }
 
-    /**
-     * This version is for rendering sub-queries, union queries, etc.
-     *
-     * @param renderingContext the rendering context
-     * @return a rendered select statement and parameters
-     */
-    @NotNull
-    public SelectStatementProvider render(RenderingContext renderingContext) {
+    public SelectStatementProvider render(RenderingStrategy renderingStrategy) {
         return SelectRenderer.withSelectModel(this)
-                .withRenderingContext(renderingContext)
+                .withRenderingStrategy(renderingStrategy)
                 .build()
                 .render();
     }
@@ -68,6 +65,8 @@ public static Builder withQueryExpressions(List<QueryExpressionModel> queryExpre
 
     public static class Builder extends AbstractBuilder<Builder> {
         private final List<QueryExpressionModel> queryExpressions = new ArrayList<>();
+        private @Nullable String forClause;
+        private @Nullable String waitClause;
 
         public Builder withQueryExpression(QueryExpressionModel queryExpression) {
             this.queryExpressions.add(queryExpression);
@@ -79,6 +78,16 @@ public Builder withQueryExpressions(List<QueryExpressionModel> queryExpressions)
             return this;
         }
 
+        public Builder withForClause(@Nullable String forClause) {
+            this.forClause = forClause;
+            return this;
+        }
+
+        public Builder withWaitClause(@Nullable String waitClause) {
+            this.waitClause = waitClause;
+            return this;
+        }
+
         @Override
         protected Builder getThis() {
             return 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 9620020e2..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,6 +18,8 @@
 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
@@ -28,30 +30,25 @@
 public class SimpleSortSpecification implements SortSpecification {
 
     private final String name;
-    private final boolean isDescending;
+    private final String descendingPhrase;
 
     private SimpleSortSpecification(String name) {
-        this(name, false);
+        this(name, ""); //$NON-NLS-1$
     }
 
-    private SimpleSortSpecification(String name, boolean isDescending) {
+    private SimpleSortSpecification(String name, String descendingPhrase) {
         this.name = Objects.requireNonNull(name);
-        this.isDescending = isDescending;
+        this.descendingPhrase = descendingPhrase;
     }
 
     @Override
     public SortSpecification descending() {
-        return new SimpleSortSpecification(name, true);
+        return new SimpleSortSpecification(name, " DESC"); //$NON-NLS-1$
     }
 
     @Override
-    public String orderByName() {
-        return name;
-    }
-
-    @Override
-    public boolean isDescending() {
-        return isDescending;
+    public FragmentAndParameters renderForOrderBy(RenderingContext renderingContext) {
+        return FragmentAndParameters.fromFragment(name + descendingPhrase);
     }
 
     public static SimpleSortSpecification of(String 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
index 27bca6fca..c2264c162 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/SubQuery.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/SubQuery.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,12 +18,13 @@
 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 String alias;
+    private final @Nullable String alias;
 
     private SubQuery(Builder builder) {
         selectModel = Objects.requireNonNull(builder.selectModel);
@@ -49,15 +50,15 @@ public <R> R accept(TableExpressionVisitor<R> visitor) {
     }
 
     public static class Builder {
-        private SelectModel selectModel;
-        private String alias;
+        private @Nullable SelectModel selectModel;
+        private @Nullable String alias;
 
         public Builder withSelectModel(SelectModel selectModel) {
             this.selectModel = selectModel;
             return this;
         }
 
-        public Builder withAlias(String alias) {
+        public Builder withAlias(@Nullable String alias) {
             this.alias = alias;
             return 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
index 8d8f07be6..6806d4791 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/UnionQuery.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/UnionQuery.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,20 +17,9 @@
 
 import java.util.Objects;
 
-public class UnionQuery {
-    private final String connector;
-    private final SelectModel selectModel;
-
+public record UnionQuery(String connector, SelectModel selectModel) {
     public UnionQuery(String connector, SelectModel selectModel) {
         this.connector = Objects.requireNonNull(connector);
         this.selectModel = Objects.requireNonNull(selectModel);
     }
-
-    public String connector() {
-        return connector;
-    }
-
-    public SelectModel selectModel() {
-        return selectModel;
-    }
 }
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
index f0431f026..fe40329ea 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/aggregate/AbstractCount.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/aggregate/AbstractCount.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,6 +17,7 @@
 
 import java.util.Optional;
 
+import org.jspecify.annotations.Nullable;
 import org.mybatis.dynamic.sql.BindableColumn;
 
 /**
@@ -25,13 +26,13 @@
  * as it is assumed that the count functions always return a number.
  */
 public abstract class AbstractCount implements BindableColumn<Long> {
-    private final String alias;
+    private final @Nullable String alias;
 
     protected AbstractCount() {
         this(null);
     }
 
-    protected AbstractCount(String alias) {
+    protected AbstractCount(@Nullable String alias) {
         this.alias = 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 5130abf7c..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,6 +15,7 @@
  */
 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;
@@ -22,7 +23,7 @@
 
 public class Avg<T> extends AbstractUniTypeFunction<T, Avg<T>> {
 
-    private Avg(BindableColumn<T> column) {
+    private Avg(BasicColumn column) {
         super(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 4c0240d24..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 78fbca6e5..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 79d9b874a..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 f44225f12..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,6 +15,7 @@
  */
 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;
@@ -22,7 +23,7 @@
 
 public class Max<T> extends AbstractUniTypeFunction<T, Max<T>> {
 
-    private Max(BindableColumn<T> column) {
+    private Max(BasicColumn column) {
         super(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 02830335c..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,6 +15,7 @@
  */
 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;
@@ -22,7 +23,7 @@
 
 public class Min<T> extends AbstractUniTypeFunction<T, Min<T>> {
 
-    private Min(BindableColumn<T> column) {
+    private Min(BasicColumn column) {
         super(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 02c8c7a42..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,8 +17,9 @@
 
 import java.util.function.Function;
 
+import org.mybatis.dynamic.sql.BasicColumn;
 import org.mybatis.dynamic.sql.BindableColumn;
-import org.mybatis.dynamic.sql.VisitableCondition;
+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;
@@ -28,12 +29,12 @@
 public class Sum<T> extends AbstractUniTypeFunction<T, Sum<T>> {
     private final Function<RenderingContext, FragmentAndParameters> renderer;
 
-    private Sum(BindableColumn<T> column) {
+    private Sum(BasicColumn column) {
         super(column);
         renderer = rc -> column.render(rc).mapFragment(this::applyAggregate);
     }
 
-    private Sum(BindableColumn<T> column, VisitableCondition<T> condition) {
+    private Sum(BindableColumn<T> column, RenderableCondition<T> condition) {
         super(column);
         renderer = rc -> {
             Validator.assertTrue(condition.shouldRender(rc), "ERROR.37", "sum"); //$NON-NLS-1$ //$NON-NLS-2$
@@ -48,7 +49,7 @@ private Sum(BindableColumn<T> column, VisitableCondition<T> condition) {
         };
     }
 
-    private Sum(BindableColumn<T> column, Function<RenderingContext, FragmentAndParameters> renderer) {
+    private Sum(BasicColumn column, Function<RenderingContext, FragmentAndParameters> renderer) {
         super(column);
         this.renderer = renderer;
     }
@@ -71,7 +72,11 @@ public static <T> Sum<T> of(BindableColumn<T> column) {
         return new Sum<>(column);
     }
 
-    public static <T> Sum<T> of(BindableColumn<T> column, VisitableCondition<T> condition) {
+    public static Sum<Object> of(BasicColumn column) {
+        return new Sum<>(column);
+    }
+
+    public static <T> Sum<T> of(BindableColumn<T> column, RenderableCondition<T> 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
index 8fe46e5fd..dcfaddc43 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/BasicWhenCondition.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/BasicWhenCondition.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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
index 16af8f891..78f841b1d 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ConditionBasedWhenCondition.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ConditionBasedWhenCondition.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -20,17 +20,17 @@
 import java.util.stream.Stream;
 
 import org.mybatis.dynamic.sql.BasicColumn;
-import org.mybatis.dynamic.sql.VisitableCondition;
+import org.mybatis.dynamic.sql.RenderableCondition;
 
 public class ConditionBasedWhenCondition<T> extends SimpleCaseWhenCondition<T> {
-    private final List<VisitableCondition<T>> conditions = new ArrayList<>();
+    private final List<RenderableCondition<T>> conditions = new ArrayList<>();
 
-    public ConditionBasedWhenCondition(List<VisitableCondition<T>> conditions, BasicColumn thenValue) {
+    public ConditionBasedWhenCondition(List<RenderableCondition<T>> conditions, BasicColumn thenValue) {
         super(thenValue);
         this.conditions.addAll(conditions);
     }
 
-    public Stream<VisitableCondition<T>> conditions() {
+    public Stream<RenderableCondition<T>> conditions() {
         return conditions.stream();
     }
 
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
index abe51d763..39d0bc740 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ElseDSL.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ElseDSL.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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
index 674012d3c..c86bf7ff0 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseDSL.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseDSL.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,25 +19,26 @@
 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.VisitableCondition;
 import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionDSL;
 
 public class SearchedCaseDSL implements ElseDSL<SearchedCaseDSL.SearchedCaseEnder> {
     private final List<SearchedCaseWhenCondition> whenConditions = new ArrayList<>();
-    private BasicColumn elseValue;
+    private @Nullable BasicColumn elseValue;
 
-    public <T> WhenDSL when(BindableColumn<T> column, VisitableCondition<T> condition,
+    public <T> WhenDSL when(BindableColumn<T> column, RenderableCondition<T> condition,
                             AndOrCriteriaGroup... subCriteria) {
         return when(column, condition, Arrays.asList(subCriteria));
     }
 
-    public <T> WhenDSL when(BindableColumn<T> column, VisitableCondition<T> condition,
+    public <T> WhenDSL when(BindableColumn<T> column, RenderableCondition<T> condition,
                             List<AndOrCriteriaGroup> subCriteria) {
         SqlCriterion sqlCriterion = ColumnAndConditionCriterion.withColumn(column)
                 .withCondition(condition)
@@ -71,7 +72,7 @@ public SearchedCaseEnder else_(BasicColumn column) {
         return new SearchedCaseEnder();
     }
 
-    public BasicColumn end() {
+    public SearchedCaseModel end() {
         return new SearchedCaseModel.Builder()
                 .withElseValue(elseValue)
                 .withWhenConditions(whenConditions)
@@ -100,7 +101,7 @@ protected WhenDSL getThis() {
     }
 
     public class SearchedCaseEnder {
-        public BasicColumn end() {
+        public SearchedCaseModel end() {
             return SearchedCaseDSL.this.end();
         }
     }
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
index 65d3f949f..c42372868 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseModel.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseModel.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -20,21 +20,25 @@
 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 {
+public class SearchedCaseModel implements BasicColumn, SortSpecification {
     private final List<SearchedCaseWhenCondition> whenConditions;
-    private final BasicColumn elseValue;
-    private final String alias;
+    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$
     }
 
@@ -56,9 +60,24 @@ 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();
@@ -66,24 +85,30 @@ public FragmentAndParameters render(RenderingContext renderingContext) {
 
     public static class Builder {
         private final List<SearchedCaseWhenCondition> whenConditions = new ArrayList<>();
-        private BasicColumn elseValue;
-        private String alias;
+        private @Nullable BasicColumn elseValue;
+        private @Nullable String alias;
+        private String descendingPhrase = ""; //$NON-NLS-1$
 
         public Builder withWhenConditions(List<SearchedCaseWhenCondition> whenConditions) {
             this.whenConditions.addAll(whenConditions);
             return this;
         }
 
-        public Builder withElseValue(BasicColumn elseValue) {
+        public Builder withElseValue(@Nullable BasicColumn elseValue) {
             this.elseValue = elseValue;
             return this;
         }
 
-        public Builder withAlias(String alias) {
+        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
index e6d57e32c..9b251be9c 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseWhenCondition.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseWhenCondition.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,6 +17,7 @@
 
 import java.util.Objects;
 
+import org.jspecify.annotations.Nullable;
 import org.mybatis.dynamic.sql.BasicColumn;
 import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionModel;
 
@@ -33,7 +34,7 @@ private SearchedCaseWhenCondition(Builder builder) {
     }
 
     public static class Builder extends AbstractBuilder<Builder> {
-        private BasicColumn thenValue;
+        private @Nullable BasicColumn thenValue;
 
         public Builder withThenValue(BasicColumn thenValue) {
             this.thenValue = thenValue;
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
index 83e46473a..be2e1e908 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseDSL.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseDSL.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -20,27 +20,28 @@
 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.VisitableCondition;
+import org.mybatis.dynamic.sql.RenderableCondition;
 
 public class SimpleCaseDSL<T> implements ElseDSL<SimpleCaseDSL<T>.SimpleCaseEnder> {
     private final BindableColumn<T> column;
     private final List<SimpleCaseWhenCondition<T>> whenConditions = new ArrayList<>();
-    private BasicColumn elseValue;
+    private @Nullable BasicColumn elseValue;
 
     private SimpleCaseDSL(BindableColumn<T> column) {
         this.column = Objects.requireNonNull(column);
     }
 
     @SafeVarargs
-    public final ConditionBasedWhenFinisher when(VisitableCondition<T> condition,
-                                                 VisitableCondition<T>... subsequentConditions) {
+    public final ConditionBasedWhenFinisher when(RenderableCondition<T> condition,
+                                                 RenderableCondition<T>... subsequentConditions) {
         return when(condition, Arrays.asList(subsequentConditions));
     }
 
-    public ConditionBasedWhenFinisher when(VisitableCondition<T> condition,
-                                           List<VisitableCondition<T>> subsequentConditions) {
+    public ConditionBasedWhenFinisher when(RenderableCondition<T> condition,
+                                           List<RenderableCondition<T>> subsequentConditions) {
         return new ConditionBasedWhenFinisher(condition, subsequentConditions);
     }
 
@@ -60,7 +61,7 @@ public SimpleCaseEnder else_(BasicColumn column) {
         return new SimpleCaseEnder();
     }
 
-    public BasicColumn end() {
+    public SimpleCaseModel<T> end() {
         return new SimpleCaseModel.Builder<T>()
                 .withColumn(column)
                 .withWhenConditions(whenConditions)
@@ -69,10 +70,10 @@ public BasicColumn end() {
     }
 
     public class ConditionBasedWhenFinisher implements ThenDSL<SimpleCaseDSL<T>> {
-        private final List<VisitableCondition<T>> conditions = new ArrayList<>();
+        private final List<RenderableCondition<T>> conditions = new ArrayList<>();
 
-        private ConditionBasedWhenFinisher(VisitableCondition<T> condition,
-                                           List<VisitableCondition<T>> subsequentConditions) {
+        private ConditionBasedWhenFinisher(RenderableCondition<T> condition,
+                                           List<RenderableCondition<T>> subsequentConditions) {
             conditions.add(condition);
             conditions.addAll(subsequentConditions);
         }
@@ -100,7 +101,7 @@ public SimpleCaseDSL<T> then(BasicColumn column) {
     }
 
     public class SimpleCaseEnder {
-        public BasicColumn end() {
+        public SimpleCaseModel<T> end() {
             return SimpleCaseDSL.this.end();
         }
     }
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
index 4b71407ae..45eb95c01 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseModel.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseModel.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -21,24 +21,28 @@
 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<T> implements BasicColumn {
+public class SimpleCaseModel<T> implements BasicColumn, SortSpecification {
     private final BindableColumn<T> column;
     private final List<SimpleCaseWhenCondition<T>> whenConditions;
-    private final BasicColumn elseValue;
-    private final String alias;
+    private final @Nullable BasicColumn elseValue;
+    private final @Nullable String alias;
+    private final String descendingPhrase;
 
     private SimpleCaseModel(Builder<T> 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$
     }
 
@@ -66,19 +70,37 @@ public SimpleCaseModel<T> as(String alias) {
                 .withWhenConditions(whenConditions)
                 .withElseValue(elseValue)
                 .withAlias(alias)
+                .withDescendingPhrase(descendingPhrase)
                 .build();
     }
 
+    @Override
+    public SimpleCaseModel<T> descending() {
+        return new Builder<T>()
+                .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<T> {
-        private BindableColumn<T> column;
+        private @Nullable BindableColumn<T> column;
         private final List<SimpleCaseWhenCondition<T>> whenConditions = new ArrayList<>();
-        private BasicColumn elseValue;
-        private String alias;
+        private @Nullable BasicColumn elseValue;
+        private @Nullable String alias;
+        private String descendingPhrase = ""; //$NON-NLS-1$
 
         public Builder<T> withColumn(BindableColumn<T> column) {
             this.column = column;
@@ -90,16 +112,21 @@ public Builder<T> withWhenConditions(List<SimpleCaseWhenCondition<T>> whenCondit
             return this;
         }
 
-        public Builder<T> withElseValue(BasicColumn elseValue) {
+        public Builder<T> withElseValue(@Nullable BasicColumn elseValue) {
             this.elseValue = elseValue;
             return this;
         }
 
-        public Builder<T> withAlias(String alias) {
+        public Builder<T> withAlias(@Nullable String alias) {
             this.alias = alias;
             return this;
         }
 
+        public Builder<T> withDescendingPhrase(String descendingPhrase) {
+            this.descendingPhrase = descendingPhrase;
+            return this;
+        }
+
         public SimpleCaseModel<T> 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
index 5466f2f3f..7f6351e02 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseWhenCondition.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseWhenCondition.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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
index 890343265..dadef7455 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseWhenConditionVisitor.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseWhenConditionVisitor.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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
index e88fb13c3..29c914731 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ThenDSL.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ThenDSL.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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/AbstractTypeConvertingFunction.java b/src/main/java/org/mybatis/dynamic/sql/select/function/AbstractTypeConvertingFunction.java
index cab8aff39..1517d3519 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/function/AbstractTypeConvertingFunction.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/function/AbstractTypeConvertingFunction.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,6 +18,9 @@
 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;
 
 /**
@@ -36,10 +39,10 @@
  */
 public abstract class AbstractTypeConvertingFunction<T, R, U extends AbstractTypeConvertingFunction<T, R, U>>
         implements BindableColumn<R> {
-    protected final BindableColumn<T> column;
-    protected String alias;
+    protected final BasicColumn column;
+    protected @Nullable String alias;
 
-    protected AbstractTypeConvertingFunction(BindableColumn<T> column) {
+    protected AbstractTypeConvertingFunction(BasicColumn column) {
         this.column = Objects.requireNonNull(column);
     }
 
@@ -48,6 +51,7 @@ public Optional<String> alias() {
         return Optional.ofNullable(alias);
     }
 
+    @NonNull
     @Override
     public U as(String alias) {
         U newThing = copy();
diff --git a/src/main/java/org/mybatis/dynamic/sql/select/function/AbstractUniTypeFunction.java b/src/main/java/org/mybatis/dynamic/sql/select/function/AbstractUniTypeFunction.java
index 7a0690bae..0ad9ee576 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/function/AbstractUniTypeFunction.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/function/AbstractUniTypeFunction.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,7 +18,7 @@
 import java.sql.JDBCType;
 import java.util.Optional;
 
-import org.mybatis.dynamic.sql.BindableColumn;
+import org.mybatis.dynamic.sql.BasicColumn;
 
 /**
  * Represents a function that does not change the underlying data type.
@@ -33,7 +33,7 @@
 public abstract class AbstractUniTypeFunction<T, U extends AbstractUniTypeFunction<T, U>>
         extends AbstractTypeConvertingFunction<T, T, U> {
 
-    protected AbstractUniTypeFunction(BindableColumn<T> column) {
+    protected AbstractUniTypeFunction(BasicColumn column) {
         super(column);
     }
 
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 fe0661bea..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -23,7 +23,7 @@
 
 public class Add<T> extends OperatorFunction<T> {
 
-    private Add(BindableColumn<T> firstColumn, BasicColumn secondColumn,
+    private Add(BasicColumn firstColumn, BasicColumn secondColumn,
             List<BasicColumn> subsequentColumns) {
         super("+", firstColumn, secondColumn, subsequentColumns); //$NON-NLS-1$
     }
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
index 11c31e352..1349d4f51 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/function/Cast.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/function/Cast.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,6 +18,7 @@
 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;
@@ -25,7 +26,7 @@
 public class Cast implements BasicColumn {
     private final BasicColumn column;
     private final String targetType;
-    private final String alias;
+    private final @Nullable String alias;
 
     private Cast(Builder builder) {
         column = Objects.requireNonNull(builder.column);
@@ -56,9 +57,9 @@ private String applyCast(String in) {
     }
 
     public static class Builder {
-        private BasicColumn column;
-        private String targetType;
-        private String alias;
+        private @Nullable BasicColumn column;
+        private @Nullable String targetType;
+        private @Nullable String alias;
 
         public Builder withColumn(BasicColumn column) {
             this.column = column;
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
index e633b2953..2dbf84b7b 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/function/Concat.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/function/Concat.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -29,7 +29,7 @@
 public class Concat<T> extends AbstractUniTypeFunction<T, Concat<T>> {
     private final List<BasicColumn> allColumns = new ArrayList<>();
 
-    protected Concat(BindableColumn<T> firstColumn, List<BasicColumn> subsequentColumns) {
+    protected Concat(BasicColumn firstColumn, List<BasicColumn> subsequentColumns) {
         super(firstColumn);
         allColumns.add(firstColumn);
         this.allColumns.addAll(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
index 3179f2a1d..16357b21f 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/function/Concatenate.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/function/Concatenate.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -23,7 +23,7 @@
 
 public class Concatenate<T> extends OperatorFunction<T> {
 
-    protected Concatenate(BindableColumn<T> firstColumn, BasicColumn secondColumn,
+    protected Concatenate(BasicColumn firstColumn, BasicColumn secondColumn,
             List<BasicColumn> subsequentColumns) {
         super("||", firstColumn, secondColumn, subsequentColumns); //$NON-NLS-1$
     }
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 24d2832ca..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -23,7 +23,7 @@
 
 public class Divide<T> extends OperatorFunction<T> {
 
-    private Divide(BindableColumn<T> firstColumn, BasicColumn secondColumn,
+    private Divide(BasicColumn firstColumn, BasicColumn secondColumn,
             List<BasicColumn> subsequentColumns) {
         super("/", firstColumn, secondColumn, subsequentColumns); //$NON-NLS-1$
     }
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 e59d04712..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,13 +15,14 @@
  */
 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.RenderingContext;
 import org.mybatis.dynamic.sql.util.FragmentAndParameters;
 
 public class Lower<T> extends AbstractUniTypeFunction<T, Lower<T>> {
 
-    private Lower(BindableColumn<T> column) {
+    private Lower(BasicColumn column) {
         super(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 50d38b966..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -23,7 +23,7 @@
 
 public class Multiply<T> extends OperatorFunction<T> {
 
-    private Multiply(BindableColumn<T> firstColumn, BasicColumn secondColumn,
+    private Multiply(BasicColumn firstColumn, BasicColumn secondColumn,
             List<BasicColumn> subsequentColumns) {
         super("*", firstColumn, secondColumn, subsequentColumns); //$NON-NLS-1$
     }
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
index 7ce5c5c25..154a5f564 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/function/OperatorFunction.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/function/OperatorFunction.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -35,7 +35,7 @@ public class OperatorFunction<T> extends AbstractUniTypeFunction<T, OperatorFunc
     protected final List<BasicColumn> subsequentColumns = new ArrayList<>();
     private final String operator;
 
-    protected OperatorFunction(String operator, BindableColumn<T> firstColumn, BasicColumn secondColumn,
+    protected OperatorFunction(String operator, BasicColumn firstColumn, BasicColumn secondColumn,
             List<BasicColumn> subsequentColumns) {
         super(firstColumn);
         this.secondColumn = Objects.requireNonNull(secondColumn);
@@ -52,9 +52,7 @@ protected OperatorFunction<T> copy() {
     public FragmentAndParameters render(RenderingContext renderingContext) {
         String paddedOperator = " " + operator + " "; //$NON-NLS-1$ //$NON-NLS-2$
 
-        // note - the cast below is added for type inference issues in some compilers
-        return Stream.of(Stream.of((BasicColumn) column),
-                        Stream.of(secondColumn), subsequentColumns.stream())
+        return Stream.of(Stream.of(column, secondColumn), subsequentColumns.stream())
                 .flatMap(Function.identity())
                 .map(column -> column.render(renderingContext))
                 .collect(FragmentCollector.collect())
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 81b95ea5c..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,6 +15,7 @@
  */
 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.RenderingContext;
 import org.mybatis.dynamic.sql.util.FragmentAndParameters;
@@ -24,7 +25,7 @@ public class Substring<T> extends AbstractUniTypeFunction<T, Substring<T>> {
     private final int offset;
     private final int length;
 
-    private Substring(BindableColumn<T> column, int offset, int length) {
+    private Substring(BasicColumn column, int offset, int length) {
         super(column);
         this.offset = offset;
         this.length = 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 94e7e3c81..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -23,7 +23,7 @@
 
 public class Subtract<T> extends OperatorFunction<T> {
 
-    private Subtract(BindableColumn<T> firstColumn, BasicColumn secondColumn,
+    private Subtract(BasicColumn firstColumn, BasicColumn secondColumn,
             List<BasicColumn> subsequentColumns) {
         super("-", firstColumn, secondColumn, subsequentColumns); //$NON-NLS-1$
     }
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 2891032df..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,13 +15,14 @@
  */
 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.RenderingContext;
 import org.mybatis.dynamic.sql.util.FragmentAndParameters;
 
 public class Upper<T> extends AbstractUniTypeFunction<T, Upper<T>> {
 
-    private Upper(BindableColumn<T> column) {
+    private Upper(BasicColumn column) {
         super(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/ColumnBasedJoinCondition.java b/src/main/java/org/mybatis/dynamic/sql/select/join/ColumnBasedJoinCondition.java
deleted file mode 100644
index 712010ca6..000000000
--- a/src/main/java/org/mybatis/dynamic/sql/select/join/ColumnBasedJoinCondition.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- *    Copyright 2016-2024 the original author or authors.
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *       https://www.apache.org/licenses/LICENSE-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 abstract class ColumnBasedJoinCondition<T> implements JoinCondition<T> {
-    private final BasicColumn rightColumn;
-
-    protected ColumnBasedJoinCondition(BasicColumn rightColumn) {
-        this.rightColumn = Objects.requireNonNull(rightColumn);
-    }
-
-    public BasicColumn rightColumn() {
-        return rightColumn;
-    }
-
-    @Override
-    public <R> R accept(JoinConditionVisitor<T, R> visitor) {
-        return visitor.visit(this);
-    }
-}
diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/EqualTo.java b/src/main/java/org/mybatis/dynamic/sql/select/join/EqualTo.java
deleted file mode 100644
index 6f12eb052..000000000
--- a/src/main/java/org/mybatis/dynamic/sql/select/join/EqualTo.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- *    Copyright 2016-2024 the original author or authors.
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *       https://www.apache.org/licenses/LICENSE-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 org.mybatis.dynamic.sql.BasicColumn;
-
-public class EqualTo<T> extends ColumnBasedJoinCondition<T> {
-
-    public EqualTo(BasicColumn rightColumn) {
-        super(rightColumn);
-    }
-
-    @Override
-    public String operator() {
-        return "="; //$NON-NLS-1$
-    }
-}
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 81925f0e4..000000000
--- a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinCriterion.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- *    Copyright 2016-2024 the original author or authors.
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *       https://www.apache.org/licenses/LICENSE-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.BindableColumn;
-
-public class JoinCriterion<T> {
-
-    private final String connector;
-    private final BindableColumn<T> leftColumn;
-    private final JoinCondition<T> joinCondition;
-
-    private JoinCriterion(Builder<T> builder) {
-        connector = Objects.requireNonNull(builder.connector);
-        leftColumn = Objects.requireNonNull(builder.joinColumn);
-        joinCondition = Objects.requireNonNull(builder.joinCondition);
-    }
-
-    public String connector() {
-        return connector;
-    }
-
-    public BindableColumn<T> leftColumn() {
-        return leftColumn;
-    }
-
-    public JoinCondition<T> joinCondition() {
-        return joinCondition;
-    }
-
-    public static class Builder<T> {
-        private String connector;
-        private BindableColumn<T> joinColumn;
-        private JoinCondition<T> joinCondition;
-
-        public Builder<T> withConnector(String connector) {
-            this.connector = connector;
-            return this;
-        }
-
-        public Builder<T> withJoinColumn(BindableColumn<T> joinColumn) {
-            this.joinColumn = joinColumn;
-            return this;
-        }
-
-        public Builder<T> withJoinCondition(JoinCondition<T> joinCondition) {
-            this.joinCondition = joinCondition;
-            return this;
-        }
-
-        public JoinCriterion<T> 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 ef1265467..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -20,13 +20,14 @@
 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 final List<JoinSpecification> joinSpecifications = new ArrayList<>();
 
-    private JoinModel(List<JoinSpecification> joinSpecifications) {
+    private JoinModel(@Nullable List<JoinSpecification> joinSpecifications) {
         Objects.requireNonNull(joinSpecifications);
         Validator.assertNotEmpty(joinSpecifications, "ERROR.15"); //$NON-NLS-1$
         this.joinSpecifications.addAll(joinSpecifications);
@@ -36,7 +37,7 @@ public Stream<JoinSpecification> joinSpecifications() {
         return joinSpecifications.stream();
     }
 
-    public static JoinModel of(List<JoinSpecification> joinSpecifications) {
+    public static JoinModel of(@Nullable List<JoinSpecification> joinSpecifications) {
         return new JoinModel(joinSpecifications);
     }
 
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 ab95dcf5e..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,36 +15,30 @@
  */
 package org.mybatis.dynamic.sql.select.join;
 
-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.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 List<JoinCriterion<?>> joinCriteria;
     private final JoinType joinType;
 
     private JoinSpecification(Builder builder) {
+        super(builder);
         table = Objects.requireNonNull(builder.table);
-        joinCriteria = Objects.requireNonNull(builder.joinCriteria);
         joinType = Objects.requireNonNull(builder.joinType);
-        Validator.assertNotEmpty(joinCriteria, "ERROR.16"); //$NON-NLS-1$
+        Validator.assertFalse(initialCriterion().isEmpty() && subCriteria().isEmpty(),
+                "ERROR.16"); //$NON-NLS-1$
     }
 
     public TableExpression table() {
         return table;
     }
 
-    @SuppressWarnings("java:S1452")
-    public Stream<JoinCriterion<?>> joinCriteria() {
-        return joinCriteria.stream();
-    }
-
     public JoinType joinType() {
         return joinType;
     }
@@ -53,26 +47,15 @@ public static Builder withJoinTable(TableExpression table) {
         return new Builder().withJoinTable(table);
     }
 
-    public static class Builder {
-        private TableExpression table;
-        private final List<JoinCriterion<?>> joinCriteria = new ArrayList<>();
-        private JoinType joinType;
+    public static class Builder extends AbstractBuilder<Builder> {
+        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<JoinCriterion<?>> joinCriteria) {
-            this.joinCriteria.addAll(joinCriteria);
-            return this;
-        }
-
         public Builder withJoinType(JoinType joinType) {
             this.joinType = joinType;
             return this;
@@ -81,5 +64,10 @@ public Builder withJoinType(JoinType joinType) {
         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 a6a456a71..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinCondition.java b/src/main/java/org/mybatis/dynamic/sql/select/join/package-info.java
similarity index 78%
rename from src/main/java/org/mybatis/dynamic/sql/select/join/JoinCondition.java
rename to src/main/java/org/mybatis/dynamic/sql/select/join/package-info.java
index 183bd9a02..03ec51bcc 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinCondition.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/join/package-info.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -13,10 +13,7 @@
  *    See the License for the specific language governing permissions and
  *    limitations under the License.
  */
+@NullMarked
 package org.mybatis.dynamic.sql.select.join;
 
-public interface JoinCondition<T> {
-    String operator();
-
-    <R> R accept(JoinConditionVisitor<T, R> visitor);
-}
+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 2542f80d0..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -20,6 +20,8 @@
 import java.util.Map;
 import java.util.Objects;
 
+import org.jspecify.annotations.Nullable;
+
 public class DefaultSelectStatementProvider implements SelectStatementProvider {
     private final String selectStatement;
     private final Map<String, Object> parameters;
@@ -44,7 +46,7 @@ public static Builder withSelectStatement(String selectStatement) {
     }
 
     public static class Builder {
-        private String selectStatement;
+        private @Nullable String selectStatement;
         private final Map<String, Object> parameters = new HashMap<>();
 
         public Builder withSelectStatement(String selectStatement) {
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 fcd62264b..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -51,30 +51,30 @@ private FragmentAndParameters renderFetchFirstRowsOnly() {
     }
 
     private FragmentAndParameters renderFetchFirstRowsOnly(Long fetchFirstRows) {
-        RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo();
+        RenderedParameterInfo fetchFirstParameterInfo = renderingContext.calculateFetchFirstRowsParameterInfo();
         return FragmentAndParameters
-                .withFragment("fetch first " + parameterInfo.renderedPlaceHolder() //$NON-NLS-1$
+                .withFragment("fetch first " + fetchFirstParameterInfo.renderedPlaceHolder() //$NON-NLS-1$
                     + " rows only") //$NON-NLS-1$
-                .withParameter(parameterInfo.parameterMapKey(), fetchFirstRows)
+                .withParameter(fetchFirstParameterInfo.parameterMapKey(), fetchFirstRows)
                 .build();
     }
 
     private FragmentAndParameters renderOffsetOnly(Long offset) {
-        RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo();
-        return FragmentAndParameters.withFragment("offset " + parameterInfo.renderedPlaceHolder() //$NON-NLS-1$
+        RenderedParameterInfo offsetParameterInfo = renderingContext.calculateOffsetParameterInfo();
+        return FragmentAndParameters.withFragment("offset " + offsetParameterInfo.renderedPlaceHolder() //$NON-NLS-1$
                 + " rows") //$NON-NLS-1$
-                .withParameter(parameterInfo.parameterMapKey(), offset)
+                .withParameter(offsetParameterInfo.parameterMapKey(), offset)
                 .build();
     }
 
     private FragmentAndParameters renderOffsetAndFetchFirstRows(Long offset, Long fetchFirstRows) {
-        RenderedParameterInfo parameterInfo1 = renderingContext.calculateParameterInfo();
-        RenderedParameterInfo parameterInfo2 = renderingContext.calculateParameterInfo();
-        return FragmentAndParameters.withFragment("offset " + parameterInfo1.renderedPlaceHolder() //$NON-NLS-1$
-                + " rows fetch first " + parameterInfo2.renderedPlaceHolder() //$NON-NLS-1$
+        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(parameterInfo1.parameterMapKey(), offset)
-                .withParameter(parameterInfo2.parameterMapKey(), fetchFirstRows)
+                .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
index 0c8292a9e..f7feb8b3f 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/render/HavingRenderer.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/render/HavingRenderer.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/JoinConditionRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/JoinConditionRenderer.java
deleted file mode 100644
index 2cba1a951..000000000
--- a/src/main/java/org/mybatis/dynamic/sql/select/render/JoinConditionRenderer.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- *    Copyright 2016-2024 the original author or authors.
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *       https://www.apache.org/licenses/LICENSE-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 static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore;
-
-import java.util.Objects;
-
-import org.mybatis.dynamic.sql.BindableColumn;
-import org.mybatis.dynamic.sql.render.RenderedParameterInfo;
-import org.mybatis.dynamic.sql.render.RenderingContext;
-import org.mybatis.dynamic.sql.select.join.ColumnBasedJoinCondition;
-import org.mybatis.dynamic.sql.select.join.JoinConditionVisitor;
-import org.mybatis.dynamic.sql.select.join.TypedJoinCondition;
-import org.mybatis.dynamic.sql.util.FragmentAndParameters;
-
-public class JoinConditionRenderer<T> implements JoinConditionVisitor<T, FragmentAndParameters> {
-    private final BindableColumn<T> leftColumn;
-    private final RenderingContext renderingContext;
-
-    private JoinConditionRenderer(Builder<T> builder) {
-        leftColumn = Objects.requireNonNull(builder.leftColumn);
-        renderingContext = Objects.requireNonNull(builder.renderingContext);
-    }
-
-    @Override
-    public FragmentAndParameters visit(TypedJoinCondition<T> condition) {
-        RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo(leftColumn);
-
-        return FragmentAndParameters
-                .withFragment(condition.operator() + spaceBefore(parameterInfo.renderedPlaceHolder()))
-                .withParameter(parameterInfo.parameterMapKey(), condition.value())
-                .build();
-    }
-
-    @Override
-    public FragmentAndParameters visit(ColumnBasedJoinCondition<T> condition) {
-        return condition.rightColumn().render(renderingContext)
-                .mapFragment(s -> condition.operator() + spaceBefore(s));
-    }
-
-    public static class Builder<T> {
-        private BindableColumn<T> leftColumn;
-        private RenderingContext renderingContext;
-
-        public Builder<T> withLeftColumn(BindableColumn<T> leftColumn) {
-            this.leftColumn = leftColumn;
-            return this;
-        }
-
-        public Builder<T> withRenderingContext(RenderingContext renderingContext) {
-            this.renderingContext = renderingContext;
-            return this;
-        }
-
-        public JoinConditionRenderer<T> build() {
-            return new JoinConditionRenderer<>(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 667b4e299..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,17 +15,17 @@
  */
 package org.mybatis.dynamic.sql.select.render;
 
-import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore;
-
 import java.util.Objects;
 import java.util.stream.Collectors;
 
+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.JoinCriterion;
 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 final JoinModel joinModel;
@@ -46,43 +46,17 @@ public FragmentAndParameters render() {
     }
 
     private FragmentAndParameters renderJoinSpecification(JoinSpecification joinSpecification) {
-        FragmentAndParameters renderedTable = joinSpecification.table().accept(tableExpressionRenderer);
-        FragmentAndParameters renderedJoin = renderConditions(joinSpecification);
-
-        String fragment = joinSpecification.joinType().type()
-                + spaceBefore(renderedTable.fragment())
-                + spaceBefore(renderedJoin.fragment());
-
-        return FragmentAndParameters.withFragment(fragment)
-                .withParameters(renderedTable.parameters())
-                .withParameters(renderedJoin.parameters())
-                .build();
-    }
-
-    private FragmentAndParameters renderConditions(JoinSpecification joinSpecification) {
-        return joinSpecification.joinCriteria()
-                .map(this::renderCriterion)
-                .collect(FragmentCollector.collect())
-                .toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$
-    }
-
-    private <T> FragmentAndParameters renderCriterion(JoinCriterion<T> joinCriterion) {
-        FragmentAndParameters renderedColumn = joinCriterion.leftColumn().render(renderingContext);
-
-        String prefix = joinCriterion.connector()
-                + spaceBefore(renderedColumn.fragment());
-
-        JoinConditionRenderer<T> joinConditionRenderer = new JoinConditionRenderer.Builder<T>()
+        FragmentCollector fc = new FragmentCollector();
+        fc.add(FragmentAndParameters.fromFragment(joinSpecification.joinType().type()));
+        fc.add(joinSpecification.table().accept(tableExpressionRenderer));
+        fc.add(JoinSpecificationRenderer
+                .withJoinSpecification(joinSpecification)
                 .withRenderingContext(renderingContext)
-                .withLeftColumn(joinCriterion.leftColumn())
-                .build();
-
-        FragmentAndParameters suffix = joinCriterion.joinCondition().accept(joinConditionRenderer);
+                .build()
+                .render()
+                .orElseThrow(() -> new InvalidSqlException(Messages.getString("ERROR.46")))); //$NON-NLS-1$
 
-        return FragmentAndParameters.withFragment(prefix + spaceBefore(suffix.fragment()))
-                .withParameters(suffix.parameters())
-                .withParameters(renderedColumn.parameters())
-                .build();
+        return fc.toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$
     }
 
     public static Builder withJoinModel(JoinModel joinModel) {
@@ -90,9 +64,9 @@ public static Builder withJoinModel(JoinModel joinModel) {
     }
 
     public static class Builder {
-        private JoinModel joinModel;
-        private TableExpressionRenderer tableExpressionRenderer;
-        private RenderingContext renderingContext;
+        private @Nullable JoinModel joinModel;
+        private @Nullable TableExpressionRenderer tableExpressionRenderer;
+        private @Nullable RenderingContext renderingContext;
 
         public Builder withJoinModel(JoinModel joinModel) {
             this.joinModel = joinModel;
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<Builder> {
+        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 008c5a1af..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -40,19 +40,19 @@ public FragmentAndParameters render() {
     }
 
     private FragmentAndParameters renderLimitOnly() {
-        RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo();
-        return FragmentAndParameters.withFragment("limit " + parameterInfo.renderedPlaceHolder()) //$NON-NLS-1$
-                .withParameter(parameterInfo.parameterMapKey(), limit)
+        RenderedParameterInfo limitParameterInfo = renderingContext.calculateLimitParameterInfo();
+        return FragmentAndParameters.withFragment("limit " + limitParameterInfo.renderedPlaceHolder()) //$NON-NLS-1$
+                .withParameter(limitParameterInfo.parameterMapKey(), limit)
                 .build();
     }
 
     private FragmentAndParameters renderLimitAndOffset(Long offset) {
-        RenderedParameterInfo parameterInfo1 = renderingContext.calculateParameterInfo();
-        RenderedParameterInfo parameterInfo2 = renderingContext.calculateParameterInfo();
-        return FragmentAndParameters.withFragment("limit " + parameterInfo1.renderedPlaceHolder() //$NON-NLS-1$
-                    + " offset " + parameterInfo2.renderedPlaceHolder()) //$NON-NLS-1$
-                .withParameter(parameterInfo1.parameterMapKey(), limit)
-                .withParameter(parameterInfo2.parameterMapKey(), 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
index 59e926f67..4daf576d4 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/render/MultiSelectRenderer.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/render/MultiSelectRenderer.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,9 +19,9 @@
 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.configuration.StatementConfiguration;
 import org.mybatis.dynamic.sql.render.RenderingContext;
 import org.mybatis.dynamic.sql.render.RenderingStrategy;
 import org.mybatis.dynamic.sql.select.MultiSelectModel;
@@ -36,11 +36,11 @@ public class MultiSelectRenderer {
     private final RenderingContext renderingContext;
 
     private MultiSelectRenderer(Builder builder) {
+        multiSelectModel = Objects.requireNonNull(builder.multiSelectModel);
         renderingContext = RenderingContext
-                .withRenderingStrategy(builder.renderingStrategy)
-                .withStatementConfiguration(builder.statementConfiguration)
+                .withRenderingStrategy(Objects.requireNonNull(builder.renderingStrategy))
+                .withStatementConfiguration(multiSelectModel.statementConfiguration())
                 .build();
-        multiSelectModel = Objects.requireNonNull(builder.multiSelectModel);
     }
 
     public SelectStatementProvider render() {
@@ -65,21 +65,21 @@ private SelectStatementProvider toSelectStatementProvider(FragmentCollector frag
     }
 
     private FragmentAndParameters renderSelect(SelectModel selectModel) {
-        SelectStatementProvider selectStatement = selectModel.render(renderingContext);
-
-        return FragmentAndParameters
-                .withFragment("(" + selectStatement.getSelectStatement() + ")") //$NON-NLS-1$ //$NON-NLS-2$
-                .withParameters(selectStatement.getParameters())
-                .build();
+        return SubQueryRenderer.withSelectModel(selectModel)
+                .withRenderingContext(renderingContext)
+                .withPrefix("(") //$NON-NLS-1$
+                .withSuffix(")") //$NON-NLS-1$
+                .build()
+                .render();
     }
 
     private FragmentAndParameters renderSelect(UnionQuery unionQuery) {
-        SelectStatementProvider selectStatement = unionQuery.selectModel().render(renderingContext);
-
-        return FragmentAndParameters.withFragment(
-                unionQuery.connector() + " (" + selectStatement.getSelectStatement() + ")") //$NON-NLS-1$ //$NON-NLS-2$
-                .withParameters(selectStatement.getParameters())
-                .build();
+        return SubQueryRenderer.withSelectModel(unionQuery.selectModel())
+                .withRenderingContext(renderingContext)
+                .withPrefix(unionQuery.connector() + " (") //$NON-NLS-1$
+                .withSuffix(")") //$NON-NLS-1$
+                .build()
+                .render();
     }
 
     private Optional<FragmentAndParameters> renderOrderBy() {
@@ -87,7 +87,7 @@ private Optional<FragmentAndParameters> renderOrderBy() {
     }
 
     private FragmentAndParameters renderOrderBy(OrderByModel orderByModel) {
-        return new OrderByRenderer().render(orderByModel);
+        return new OrderByRenderer(renderingContext).render(orderByModel);
     }
 
     private Optional<FragmentAndParameters> renderPagingModel() {
@@ -102,10 +102,13 @@ private FragmentAndParameters renderPagingModel(PagingModel pagingModel) {
                 .render();
     }
 
+    public static Builder withMultiSelectModel(MultiSelectModel multiSelectModel) {
+        return new Builder().withMultiSelectModel(multiSelectModel);
+    }
+
     public static class Builder {
-        private RenderingStrategy renderingStrategy;
-        private MultiSelectModel multiSelectModel;
-        private StatementConfiguration statementConfiguration;
+        private @Nullable RenderingStrategy renderingStrategy;
+        private @Nullable MultiSelectModel multiSelectModel;
 
         public Builder withRenderingStrategy(RenderingStrategy renderingStrategy) {
             this.renderingStrategy = renderingStrategy;
@@ -117,11 +120,6 @@ public Builder withMultiSelectModel(MultiSelectModel multiSelectModel) {
             return this;
         }
 
-        public Builder withStatementConfiguration(StatementConfiguration statementConfiguration) {
-            this.statementConfiguration = statementConfiguration;
-            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 583b48e4b..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,6 +17,7 @@
 
 import java.util.Objects;
 
+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;
@@ -44,8 +45,8 @@ private FragmentAndParameters fetchFirstRender() {
     }
 
     public static class Builder {
-        private PagingModel pagingModel;
-        private RenderingContext renderingContext;
+        private @Nullable PagingModel pagingModel;
+        private @Nullable RenderingContext renderingContext;
 
         public Builder withRenderingContext(RenderingContext renderingContext) {
             this.renderingContext = renderingContext;
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 0f3ddcb01..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,6 +19,7 @@
 import java.util.Optional;
 import java.util.stream.Collectors;
 
+import org.jspecify.annotations.Nullable;
 import org.mybatis.dynamic.sql.BasicColumn;
 import org.mybatis.dynamic.sql.TableExpression;
 import org.mybatis.dynamic.sql.render.ExplicitTableAliasCalculator;
@@ -43,7 +44,8 @@ private QueryExpressionRenderer(Builder builder) {
         queryExpression = Objects.requireNonNull(builder.queryExpression);
         TableAliasCalculator childTableAliasCalculator = calculateChildTableAliasCalculator(queryExpression);
 
-        renderingContext = builder.renderingContext.withChildTableAliasCalculator(childTableAliasCalculator);
+        renderingContext = Objects.requireNonNull(builder.renderingContext)
+                .withChildTableAliasCalculator(childTableAliasCalculator);
 
         tableExpressionRenderer = new TableExpressionRenderer.Builder()
                 .withRenderingContext(renderingContext)
@@ -139,12 +141,8 @@ private FragmentAndParameters calculateColumnList() {
     private FragmentAndParameters renderColumnAndAlias(BasicColumn selectListItem) {
         FragmentAndParameters renderedColumn = selectListItem.render(renderingContext);
 
-        String nameAndTableAlias = selectListItem.alias().map(a -> renderedColumn.fragment() + " as " + a) //$NON-NLS-1$
-                .orElse(renderedColumn.fragment());
-
-        return FragmentAndParameters.withFragment(nameAndTableAlias)
-                .withParameters(renderedColumn.parameters())
-                .build();
+        return selectListItem.alias().map(a -> renderedColumn.mapFragment(f -> f + " as " + a)) //$NON-NLS-1$
+                .orElse(renderedColumn);
     }
 
     private FragmentAndParameters renderTableExpression(TableExpression table) {
@@ -203,8 +201,8 @@ public static Builder withQueryExpression(QueryExpressionModel model) {
     }
 
     public static class Builder {
-        private QueryExpressionModel queryExpression;
-        private RenderingContext renderingContext;
+        private @Nullable QueryExpressionModel queryExpression;
+        private @Nullable RenderingContext renderingContext;
 
         public Builder withRenderingContext(RenderingContext renderingContext) {
             this.renderingContext = renderingContext;
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
index d273c4c87..9fd58a591 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseRenderer.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseRenderer.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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
index b7d9bfff3..f73150f5f 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseWhenConditionRenderer.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseWhenConditionRenderer.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 3f65a51e4..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -16,71 +16,35 @@
 package org.mybatis.dynamic.sql.select.render;
 
 import java.util.Objects;
-import java.util.Optional;
-import java.util.stream.Collectors;
 
-import org.mybatis.dynamic.sql.common.OrderByModel;
-import org.mybatis.dynamic.sql.common.OrderByRenderer;
+import org.jspecify.annotations.Nullable;
 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.render.RenderingStrategy;
 import org.mybatis.dynamic.sql.select.SelectModel;
 import org.mybatis.dynamic.sql.util.FragmentAndParameters;
-import org.mybatis.dynamic.sql.util.FragmentCollector;
 
 public class SelectRenderer {
     private final SelectModel selectModel;
-    private final RenderingContext renderingContext;
+    private final RenderingStrategy renderingStrategy;
 
     private SelectRenderer(Builder builder) {
         selectModel = Objects.requireNonNull(builder.selectModel);
-        renderingContext = Objects.requireNonNull(builder.renderingContext);
+        renderingStrategy = Objects.requireNonNull(builder.renderingStrategy);
     }
 
     public SelectStatementProvider render() {
-        FragmentCollector fragmentCollector = selectModel
-                .queryExpressions()
-                .map(this::renderQueryExpression)
-                .collect(FragmentCollector.collect());
-
-        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())
+        RenderingContext renderingContext = RenderingContext.withRenderingStrategy(renderingStrategy)
+                .withStatementConfiguration(selectModel.statementConfiguration())
                 .build();
-    }
 
-    private FragmentAndParameters renderQueryExpression(QueryExpressionModel queryExpressionModel) {
-        return QueryExpressionRenderer.withQueryExpression(queryExpressionModel)
+        FragmentAndParameters fragmentAndParameters = SubQueryRenderer.withSelectModel(selectModel)
                 .withRenderingContext(renderingContext)
                 .build()
                 .render();
-    }
-
-    private Optional<FragmentAndParameters> renderOrderBy() {
-        return selectModel.orderByModel().map(this::renderOrderBy);
-    }
 
-    private FragmentAndParameters renderOrderBy(OrderByModel orderByModel) {
-        return new OrderByRenderer().render(orderByModel);
-    }
-
-    private Optional<FragmentAndParameters> renderPagingModel() {
-        return selectModel.pagingModel().map(this::renderPagingModel);
-    }
-
-    private FragmentAndParameters renderPagingModel(PagingModel pagingModel) {
-        return new PagingModelRenderer.Builder()
-                .withPagingModel(pagingModel)
-                .withRenderingContext(renderingContext)
-                .build()
-                .render();
+        return DefaultSelectStatementProvider.withSelectStatement(fragmentAndParameters.fragment())
+                .withParameters(fragmentAndParameters.parameters())
+                .build();
     }
 
     public static Builder withSelectModel(SelectModel selectModel) {
@@ -88,11 +52,11 @@ public static Builder withSelectModel(SelectModel selectModel) {
     }
 
     public static class Builder {
-        private SelectModel selectModel;
-        private RenderingContext renderingContext;
+        private @Nullable SelectModel selectModel;
+        private @Nullable RenderingStrategy renderingStrategy;
 
-        public Builder withRenderingContext(RenderingContext renderingContext) {
-            this.renderingContext = renderingContext;
+        public Builder withRenderingStrategy(RenderingStrategy renderingStrategy) {
+            this.renderingStrategy = renderingStrategy;
             return 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 96a8b621c..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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
index 2639d0b53..036a9c909 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -48,7 +48,9 @@ public FragmentAndParameters render() {
     }
 
     private FragmentAndParameters renderCase() {
-        return simpleCaseModel.column().render(renderingContext)
+        return simpleCaseModel.column().alias()
+                .map(FragmentAndParameters::fromFragment)
+                .orElseGet(() -> simpleCaseModel.column().render(renderingContext))
                 .mapFragment(f -> "case " + f); //$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
index e9108806e..1bb6e2adc 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseWhenConditionRenderer.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseWhenConditionRenderer.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,7 +19,7 @@
 import java.util.stream.Collectors;
 
 import org.mybatis.dynamic.sql.BindableColumn;
-import org.mybatis.dynamic.sql.VisitableCondition;
+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;
@@ -28,20 +28,14 @@
 import org.mybatis.dynamic.sql.util.FragmentAndParameters;
 import org.mybatis.dynamic.sql.util.FragmentCollector;
 import org.mybatis.dynamic.sql.util.Validator;
-import org.mybatis.dynamic.sql.where.render.DefaultConditionVisitor;
 
 public class SimpleCaseWhenConditionRenderer<T> implements SimpleCaseWhenConditionVisitor<T, FragmentAndParameters> {
     private final RenderingContext renderingContext;
     private final BindableColumn<T> column;
-    private final DefaultConditionVisitor<T> conditionVisitor;
 
     public SimpleCaseWhenConditionRenderer(RenderingContext renderingContext, BindableColumn<T> column) {
         this.renderingContext = Objects.requireNonNull(renderingContext);
         this.column = Objects.requireNonNull(column);
-        conditionVisitor = new DefaultConditionVisitor.Builder<T>()
-                .withColumn(column)
-                .withRenderingContext(renderingContext)
-                .build();
     }
 
     @Override
@@ -63,12 +57,12 @@ public FragmentAndParameters visit(BasicWhenCondition<T> whenCondition) {
                 .toFragmentAndParameters(Collectors.joining(", ")); //$NON-NLS-1$
     }
 
-    private boolean shouldRender(VisitableCondition<T> condition) {
+    private boolean shouldRender(RenderableCondition<T> condition) {
         return condition.shouldRender(renderingContext);
     }
 
-    private FragmentAndParameters renderCondition(VisitableCondition<T> condition) {
-        return condition.accept(conditionVisitor);
+    private FragmentAndParameters renderCondition(RenderableCondition<T> condition) {
+        return condition.renderCondition(renderingContext, column);
     }
 
     private FragmentAndParameters renderBasicValue(T value) {
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
index ccb9b1d84..8114e2411 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/render/TableExpressionRenderer.java
+++ b/src/main/java/org/mybatis/dynamic/sql/select/render/TableExpressionRenderer.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,10 +15,9 @@
  */
 package org.mybatis.dynamic.sql.select.render;
 
-import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore;
-
 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;
@@ -39,25 +38,19 @@ public FragmentAndParameters visit(SqlTable table) {
 
     @Override
     public FragmentAndParameters visit(SubQuery subQuery) {
-        SelectStatementProvider selectStatement = subQuery.selectModel().render(renderingContext);
-
-        String fragment = "(" + selectStatement.getSelectStatement() + ")"; //$NON-NLS-1$ //$NON-NLS-2$
-
-        fragment = applyAlias(fragment, subQuery);
-
-        return FragmentAndParameters.withFragment(fragment)
-                .withParameters(selectStatement.getParameters())
-                .build();
-    }
+        String suffix = subQuery.alias().map(a -> ") " + a) //$NON-NLS-1$
+                .orElse(")"); //$NON-NLS-1$
 
-    private String applyAlias(String fragment, SubQuery subQuery) {
-        return subQuery.alias()
-                .map(a -> fragment + spaceBefore(a))
-                .orElse(fragment);
+        return SubQueryRenderer.withSelectModel(subQuery.selectModel())
+                .withRenderingContext(renderingContext)
+                .withPrefix("(")//$NON-NLS-1$
+                .withSuffix(suffix)
+                .build()
+                .render();
     }
 
     public static class Builder {
-        private RenderingContext renderingContext;
+        private @Nullable RenderingContext renderingContext;
 
         public Builder withRenderingContext(RenderingContext renderingContext) {
             this.renderingContext = renderingContext;
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/UpdateDSL.java b/src/main/java/org/mybatis/dynamic/sql/update/UpdateDSL.java
index 8cf7259a3..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -24,7 +24,7 @@
 import java.util.function.Function;
 import java.util.function.Supplier;
 
-import org.jetbrains.annotations.NotNull;
+import org.jspecify.annotations.Nullable;
 import org.mybatis.dynamic.sql.BasicColumn;
 import org.mybatis.dynamic.sql.SortSpecification;
 import org.mybatis.dynamic.sql.SqlColumn;
@@ -39,7 +39,6 @@
 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.Utilities;
 import org.mybatis.dynamic.sql.util.ValueMapping;
 import org.mybatis.dynamic.sql.util.ValueOrNullMapping;
 import org.mybatis.dynamic.sql.util.ValueWhenPresentMapping;
@@ -47,19 +46,19 @@
 import org.mybatis.dynamic.sql.where.AbstractWhereStarter;
 import org.mybatis.dynamic.sql.where.EmbeddedWhereModel;
 
-public class UpdateDSL<R> extends AbstractWhereStarter<UpdateDSL<R>.UpdateWhereBuilder, UpdateDSL<R>>
-        implements Buildable<R> {
+public class UpdateDSL<R> implements AbstractWhereStarter<UpdateDSL<R>.UpdateWhereBuilder, UpdateDSL<R>>,
+        Buildable<R> {
 
     private final Function<UpdateModel, R> adapterFunction;
     private final List<AbstractColumnMapping> columnMappings = new ArrayList<>();
     private final SqlTable table;
-    private final String tableAlias;
-    private UpdateWhereBuilder whereBuilder;
+    private final @Nullable String tableAlias;
+    private @Nullable UpdateWhereBuilder whereBuilder;
     private final StatementConfiguration statementConfiguration = new StatementConfiguration();
-    private Long limit;
-    private OrderByModel orderByModel;
+    private @Nullable Long limit;
+    private @Nullable OrderByModel orderByModel;
 
-    private UpdateDSL(SqlTable table, String tableAlias, Function<UpdateModel, R> adapterFunction) {
+    private UpdateDSL(SqlTable table, @Nullable String tableAlias, Function<UpdateModel, R> adapterFunction) {
         this.table = Objects.requireNonNull(table);
         this.tableAlias = tableAlias;
         this.adapterFunction = Objects.requireNonNull(adapterFunction);
@@ -71,11 +70,15 @@ public <T> SetClauseFinisher<T> set(SqlColumn<T> column) {
 
     @Override
     public UpdateWhereBuilder where() {
-        whereBuilder = Utilities.buildIfNecessary(whereBuilder, UpdateWhereBuilder::new);
+        whereBuilder = Objects.requireNonNullElseGet(whereBuilder, UpdateWhereBuilder::new);
         return whereBuilder;
     }
 
     public UpdateDSL<R> limit(long limit) {
+        return limitWhenPresent(limit);
+    }
+
+    public UpdateDSL<R> limitWhenPresent(@Nullable Long limit) {
         this.limit = limit;
         return this;
     }
@@ -95,7 +98,6 @@ public UpdateDSL<R> orderBy(Collection<? extends SortSpecification> columns) {
      *
      * @return the update model
      */
-    @NotNull
     @Override
     public R build() {
         UpdateModel updateModel = UpdateModel.withTable(table)
@@ -116,7 +118,8 @@ public UpdateDSL<R> configureStatement(Consumer<StatementConfiguration> consumer
         return this;
     }
 
-    public static <R> UpdateDSL<R> update(Function<UpdateModel, R> adapterFunction, SqlTable table, String tableAlias) {
+    public static <R> UpdateDSL<R> update(Function<UpdateModel, R> adapterFunction, SqlTable table,
+                                          @Nullable String tableAlias) {
         return new UpdateDSL<>(table, tableAlias, adapterFunction);
     }
 
@@ -170,20 +173,20 @@ public UpdateDSL<R> equalTo(BasicColumn rightColumn) {
             return UpdateDSL.this;
         }
 
-        public UpdateDSL<R> equalToOrNull(T value) {
+        public UpdateDSL<R> equalToOrNull(@Nullable T value) {
             return equalToOrNull(() -> value);
         }
 
-        public UpdateDSL<R> equalToOrNull(Supplier<T> valueSupplier) {
+        public UpdateDSL<R> equalToOrNull(Supplier<@Nullable T> valueSupplier) {
             columnMappings.add(ValueOrNullMapping.of(column, valueSupplier));
             return UpdateDSL.this;
         }
 
-        public UpdateDSL<R> equalToWhenPresent(T value) {
+        public UpdateDSL<R> equalToWhenPresent(@Nullable T value) {
             return equalToWhenPresent(() -> value);
         }
 
-        public UpdateDSL<R> equalToWhenPresent(Supplier<T> valueSupplier) {
+        public UpdateDSL<R> equalToWhenPresent(Supplier<@Nullable T> valueSupplier) {
             columnMappings.add(ValueWhenPresentMapping.of(column, valueSupplier));
             return UpdateDSL.this;
         }
@@ -196,7 +199,11 @@ private UpdateWhereBuilder() {
         }
 
         public UpdateDSL<R> limit(long limit) {
-            return UpdateDSL.this.limit(limit);
+            return limitWhenPresent(limit);
+        }
+
+        public UpdateDSL<R> limitWhenPresent(@Nullable Long limit) {
+            return UpdateDSL.this.limitWhenPresent(limit);
         }
 
         public UpdateDSL<R> orderBy(SortSpecification... columns) {
@@ -208,7 +215,6 @@ public UpdateDSL<R> orderBy(Collection<? extends SortSpecification> columns) {
             return UpdateDSL.this;
         }
 
-        @NotNull
         @Override
         public R build() {
             return UpdateDSL.this.build();
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 70abc3a5e..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 7ff82df26..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -21,7 +21,7 @@
 import java.util.Optional;
 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;
@@ -35,11 +35,11 @@
 
 public class UpdateModel {
     private final SqlTable table;
-    private final String tableAlias;
-    private final EmbeddedWhereModel whereModel;
+    private final @Nullable String tableAlias;
+    private final @Nullable EmbeddedWhereModel whereModel;
     private final List<AbstractColumnMapping> columnMappings;
-    private final Long limit;
-    private final OrderByModel orderByModel;
+    private final @Nullable Long limit;
+    private final @Nullable OrderByModel orderByModel;
     private final StatementConfiguration statementConfiguration;
 
     private UpdateModel(Builder builder) {
@@ -77,11 +77,13 @@ public Optional<OrderByModel> orderByModel() {
         return Optional.ofNullable(orderByModel);
     }
 
-    @NotNull
+    public StatementConfiguration statementConfiguration() {
+        return statementConfiguration;
+    }
+
     public UpdateStatementProvider render(RenderingStrategy renderingStrategy) {
         return UpdateRenderer.withUpdateModel(this)
                 .withRenderingStrategy(renderingStrategy)
-                .withStatementConfiguration(statementConfiguration)
                 .build()
                 .render();
     }
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 a103bd604..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,13 +19,15 @@
 import java.util.Map;
 import java.util.Objects;
 
+import org.jspecify.annotations.Nullable;
+
 public class DefaultUpdateStatementProvider implements UpdateStatementProvider {
     private final String updateStatement;
-    private final Map<String, Object> parameters = new HashMap<>();
+    private final Map<String, Object> parameters;
 
     private DefaultUpdateStatementProvider(Builder builder) {
         updateStatement = Objects.requireNonNull(builder.updateStatement);
-        parameters.putAll(builder.parameters);
+        parameters = builder.parameters;
     }
 
     @Override
@@ -43,7 +45,7 @@ public static Builder withUpdateStatement(String updateStatement) {
     }
 
     public static class Builder {
-        private String updateStatement;
+        private @Nullable String updateStatement;
         private final Map<String, Object> parameters = new HashMap<>();
 
         public Builder withUpdateStatement(String updateStatement) {
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 07d955c80..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,9 +18,10 @@
 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.select.render.SelectStatementProvider;
+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;
@@ -84,31 +85,26 @@ public <T> Optional<FragmentAndParameters> visit(ValueWhenPresentMapping<T> mapp
 
     @Override
     public Optional<FragmentAndParameters> visit(SelectMapping mapping) {
-        SelectStatementProvider selectStatement = mapping.selectModel().render(renderingContext);
-        String fragment = renderingContext.aliasedColumnName(mapping.column())
-                + " = (" //$NON-NLS-1$
-                + selectStatement.getSelectStatement()
-                + ")"; //$NON-NLS-1$
+        String prefix = renderingContext.aliasedColumnName(mapping.column()) + " = ("; //$NON-NLS-1$
 
-        return FragmentAndParameters.withFragment(fragment)
-                .withParameters(selectStatement.getParameters())
-                .buildOptional();
+        FragmentAndParameters fragmentAndParameters = SubQueryRenderer.withSelectModel(mapping.selectModel())
+                .withRenderingContext(renderingContext)
+                .withPrefix(prefix)
+                .withSuffix(")") //$NON-NLS-1$
+                .build()
+                .render();
+
+        return Optional.of(fragmentAndParameters);
     }
 
     @Override
     public Optional<FragmentAndParameters> visit(ColumnToColumnMapping mapping) {
-        FragmentAndParameters renderedColumn = mapping.rightColumn().render(renderingContext);
-
-        String setPhrase = renderingContext.aliasedColumnName(mapping.column())
-                + " = "  //$NON-NLS-1$
-                + renderedColumn.fragment();
-
-        return FragmentAndParameters.withFragment(setPhrase)
-                .withParameters(renderedColumn.parameters())
-                .buildOptional();
+        FragmentAndParameters fragmentAndParameters = mapping.rightColumn().render(renderingContext)
+                .mapFragment(f -> renderingContext.aliasedColumnName(mapping.column()) + " = " + f); //$NON-NLS-1$
+        return Optional.of(fragmentAndParameters);
     }
 
-    private <T> Optional<FragmentAndParameters> buildValueFragment(AbstractColumnMapping mapping, T value) {
+    private <T> Optional<FragmentAndParameters> buildValueFragment(AbstractColumnMapping mapping, @Nullable T value) {
         RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo(mapping.column());
         String setPhrase = renderingContext.aliasedColumnName(mapping.column())
                 + " = "  //$NON-NLS-1$
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 50397ee7f..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,14 +15,13 @@
  */
 package org.mybatis.dynamic.sql.update.render;
 
-import java.util.List;
 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.configuration.StatementConfiguration;
 import org.mybatis.dynamic.sql.render.ExplicitTableAliasCalculator;
 import org.mybatis.dynamic.sql.render.RenderedParameterInfo;
 import org.mybatis.dynamic.sql.render.RenderingContext;
@@ -47,7 +46,7 @@ private UpdateRenderer(Builder builder) {
         renderingContext = RenderingContext
                 .withRenderingStrategy(Objects.requireNonNull(builder.renderingStrategy))
                 .withTableAliasCalculator(tableAliasCalculator)
-                .withStatementConfiguration(builder.statementConfiguration)
+                .withStatementConfiguration(updateModel.statementConfiguration())
                 .build();
         visitor = new SetPhraseVisitor(renderingContext);
     }
@@ -77,24 +76,15 @@ private FragmentAndParameters calculateUpdateStatementStart() {
     }
 
     private FragmentAndParameters calculateSetPhrase() {
-        List<Optional<FragmentAndParameters>> fragmentsAndParameters = updateModel.columnMappings()
-                        .map(m -> m.accept(visitor))
-                        .collect(Collectors.toList());
-
-        Validator.assertFalse(fragmentsAndParameters.stream().noneMatch(Optional::isPresent),
-                "ERROR.18"); //$NON-NLS-1$
-
-        FragmentCollector fragmentCollector = fragmentsAndParameters.stream()
-                .filter(Optional::isPresent)
-                .map(Optional::get)
+        FragmentCollector fragmentCollector = updateModel.columnMappings()
+                .map(m -> m.accept(visitor))
+                .flatMap(Optional::stream)
                 .collect(FragmentCollector.collect());
 
-        return toSetPhrase(fragmentCollector);
-    }
+        Validator.assertFalse(fragmentCollector.isEmpty(), "ERROR.18"); //$NON-NLS-1$
 
-    private FragmentAndParameters toSetPhrase(FragmentCollector fragmentCollector) {
         return fragmentCollector.toFragmentAndParameters(
-                Collectors.joining(", ", "set ", "")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+                        Collectors.joining(", ", "set ", "")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
     }
 
     private Optional<FragmentAndParameters> calculateWhereClause() {
@@ -110,7 +100,7 @@ private Optional<FragmentAndParameters> calculateLimitClause() {
     }
 
     private FragmentAndParameters renderLimitClause(Long limit) {
-        RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo();
+        RenderedParameterInfo parameterInfo = renderingContext.calculateLimitParameterInfo();
 
         return FragmentAndParameters.withFragment("limit " + parameterInfo.renderedPlaceHolder()) //$NON-NLS-1$
                 .withParameter(parameterInfo.parameterMapKey(), limit)
@@ -122,7 +112,7 @@ private Optional<FragmentAndParameters> calculateOrderByClause() {
     }
 
     private FragmentAndParameters renderOrderByClause(OrderByModel orderByModel) {
-        return new OrderByRenderer().render(orderByModel);
+        return new OrderByRenderer(renderingContext).render(orderByModel);
     }
 
     public static Builder withUpdateModel(UpdateModel updateModel) {
@@ -130,9 +120,8 @@ public static Builder withUpdateModel(UpdateModel updateModel) {
     }
 
     public static class Builder {
-        private UpdateModel updateModel;
-        private RenderingStrategy renderingStrategy;
-        private StatementConfiguration statementConfiguration;
+        private @Nullable UpdateModel updateModel;
+        private @Nullable RenderingStrategy renderingStrategy;
 
         public Builder withUpdateModel(UpdateModel updateModel) {
             this.updateModel = updateModel;
@@ -144,11 +133,6 @@ public Builder withRenderingStrategy(RenderingStrategy renderingStrategy) {
             return this;
         }
 
-        public Builder withStatementConfiguration(StatementConfiguration statementConfiguration) {
-            this.statementConfiguration = statementConfiguration;
-            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 539b41b80..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 e86e69738..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 eab57548f..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,10 +15,7 @@
  */
 package org.mybatis.dynamic.sql.util;
 
-import org.jetbrains.annotations.NotNull;
-
 @FunctionalInterface
 public interface Buildable<T> {
-    @NotNull
     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
index 37c785cd2..19f949f09 100644
--- a/src/main/java/org/mybatis/dynamic/sql/util/ColumnMappingVisitor.java
+++ b/src/main/java/org/mybatis/dynamic/sql/util/ColumnMappingVisitor.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/main/java/org/mybatis/dynamic/sql/util/ColumnToColumnMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/ColumnToColumnMapping.java
index d72607cc2..b287ce215 100644
--- a/src/main/java/org/mybatis/dynamic/sql/util/ColumnToColumnMapping.java
+++ b/src/main/java/org/mybatis/dynamic/sql/util/ColumnToColumnMapping.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/main/java/org/mybatis/dynamic/sql/util/ConfigurableStatement.java b/src/main/java/org/mybatis/dynamic/sql/util/ConfigurableStatement.java
index d87edac4b..9fedc85a0 100644
--- a/src/main/java/org/mybatis/dynamic/sql/util/ConfigurableStatement.java
+++ b/src/main/java/org/mybatis/dynamic/sql/util/ConfigurableStatement.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 c813559f3..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 c6b65b3b0..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,12 +15,15 @@
  */
 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 final String fragment;
@@ -28,7 +31,7 @@ public class FragmentAndParameters {
 
     private FragmentAndParameters(Builder builder) {
         fragment = Objects.requireNonNull(builder.fragment);
-        parameters = Objects.requireNonNull(builder.parameters);
+        parameters = Collections.unmodifiableMap(builder.parameters);
     }
 
     public String fragment() {
@@ -46,7 +49,7 @@ public Map<String, Object> parameters() {
      * @return a new instance with the same parameters and a transformed fragment
      */
     public FragmentAndParameters mapFragment(UnaryOperator<String> mapper) {
-        return FragmentAndParameters.withFragment(mapper.apply(fragment))
+        return withFragment(mapper.apply(fragment))
                 .withParameters(parameters)
                 .build();
     }
@@ -60,7 +63,7 @@ public static FragmentAndParameters fromFragment(String fragment) {
     }
 
     public static class Builder {
-        private String fragment;
+        private @Nullable String fragment;
         private final Map<String, Object> parameters = new HashMap<>();
 
         public Builder withFragment(String fragment) {
@@ -68,7 +71,10 @@ public Builder withFragment(String 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;
         }
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 cc043572f..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -16,6 +16,7 @@
 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;
@@ -23,7 +24,8 @@
 import java.util.stream.Collector;
 
 public class FragmentCollector {
-    final List<FragmentAndParameters> fragments = new ArrayList<>();
+    final List<String> fragments = new ArrayList<>();
+    final Map<String, Object> parameters = new HashMap<>();
 
     public FragmentCollector() {
         super();
@@ -34,20 +36,22 @@ private FragmentCollector(FragmentAndParameters initialFragment) {
     }
 
     public void add(FragmentAndParameters fragmentAndParameters) {
-        fragments.add(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 Optional<String> firstFragment() {
-        return fragments.stream().findFirst().map(FragmentAndParameters::fragment);
+        return fragments.stream().findFirst();
     }
 
     public String collectFragments(Collector<CharSequence, ?, String> fragmentCollector) {
-        return fragments.stream().map(FragmentAndParameters::fragment).collect(fragmentCollector);
+        return fragments.stream().collect(fragmentCollector);
     }
 
     public FragmentAndParameters toFragmentAndParameters(Collector<CharSequence, ?, String> fragmentCollector) {
@@ -57,9 +61,7 @@ public FragmentAndParameters toFragmentAndParameters(Collector<CharSequence, ?,
     }
 
     public Map<String, Object> parameters() {
-        return fragments.stream()
-                .map(FragmentAndParameters::parameters)
-                .collect(HashMap::new, HashMap::putAll, HashMap::putAll);
+        return Collections.unmodifiableMap(parameters);
     }
 
     public boolean hasMultipleFragments() {
diff --git a/src/main/java/org/mybatis/dynamic/sql/util/GeneralInsertMappingVisitor.java b/src/main/java/org/mybatis/dynamic/sql/util/GeneralInsertMappingVisitor.java
index ac20a6faf..e55e3d100 100644
--- a/src/main/java/org/mybatis/dynamic/sql/util/GeneralInsertMappingVisitor.java
+++ b/src/main/java/org/mybatis/dynamic/sql/util/GeneralInsertMappingVisitor.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 39add47a1..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/main/java/org/mybatis/dynamic/sql/util/InternalError.java b/src/main/java/org/mybatis/dynamic/sql/util/InternalError.java
index e437b8728..ed6b0a3cc 100644
--- a/src/main/java/org/mybatis/dynamic/sql/util/InternalError.java
+++ b/src/main/java/org/mybatis/dynamic/sql/util/InternalError.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/main/java/org/mybatis/dynamic/sql/util/Messages.java b/src/main/java/org/mybatis/dynamic/sql/util/Messages.java
index 1c013bb10..649dd7747 100644
--- a/src/main/java/org/mybatis/dynamic/sql/util/Messages.java
+++ b/src/main/java/org/mybatis/dynamic/sql/util/Messages.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/main/java/org/mybatis/dynamic/sql/util/MultiRowInsertMappingVisitor.java b/src/main/java/org/mybatis/dynamic/sql/util/MultiRowInsertMappingVisitor.java
index 6132ce90e..18a75221d 100644
--- a/src/main/java/org/mybatis/dynamic/sql/util/MultiRowInsertMappingVisitor.java
+++ b/src/main/java/org/mybatis/dynamic/sql/util/MultiRowInsertMappingVisitor.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 b30430f0f..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 0c66c921d..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,10 +17,12 @@
 
 import java.util.function.BiPredicate;
 
+import org.jspecify.annotations.Nullable;
+
 public class Predicates {
     private Predicates() {}
 
-    public static <T> BiPredicate<T, T> bothPresent() {
+    public static <T> 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 e0a4f6779..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/main/java/org/mybatis/dynamic/sql/util/PropertyWhenPresentMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/PropertyWhenPresentMapping.java
index 3634e6244..e5f6f3e62 100644
--- a/src/main/java/org/mybatis/dynamic/sql/util/PropertyWhenPresentMapping.java
+++ b/src/main/java/org/mybatis/dynamic/sql/util/PropertyWhenPresentMapping.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/main/java/org/mybatis/dynamic/sql/util/RowMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/RowMapping.java
index 20f99940c..9ece580ca 100644
--- a/src/main/java/org/mybatis/dynamic/sql/util/RowMapping.java
+++ b/src/main/java/org/mybatis/dynamic/sql/util/RowMapping.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 5a24c9956..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 fafcc6bda..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,7 +17,6 @@
 
 import java.util.List;
 import java.util.Map;
-import java.util.stream.Collectors;
 
 import org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider;
 import org.mybatis.dynamic.sql.insert.render.GeneralInsertStatementProvider;
@@ -76,7 +75,7 @@ public String insertMultipleWithGeneratedKeys(Map<String, Object> parameterMap)
                 .map(Map.Entry::getValue)
                 .filter(String.class::isInstance)
                 .map(String.class::cast)
-                .collect(Collectors.toList());
+                .toList();
 
         if (entries.size() == 1) {
             return entries.get(0);
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 8c2cf8ccc..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 3ef16be5e..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -25,10 +25,6 @@ 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();
 
@@ -44,7 +40,7 @@ static String toCamelCase(String inputString) {
                     sb.append(Character.toLowerCase(c));
                 }
             } else {
-                if (sb.length() > 0) {
+                if (!sb.isEmpty()) {
                     nextUpperCase = true;
                 }
             }
@@ -56,6 +52,15 @@ static String toCamelCase(String inputString) {
     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> 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 9dde23da1..8d06585ab 100644
--- a/src/main/java/org/mybatis/dynamic/sql/util/UpdateMappingVisitor.java
+++ b/src/main/java/org/mybatis/dynamic/sql/util/UpdateMappingVisitor.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/main/java/org/mybatis/dynamic/sql/util/Utilities.java b/src/main/java/org/mybatis/dynamic/sql/util/Utilities.java
index 780369175..0c3bd188f 100644
--- a/src/main/java/org/mybatis/dynamic/sql/util/Utilities.java
+++ b/src/main/java/org/mybatis/dynamic/sql/util/Utilities.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,14 +15,10 @@
  */
 package org.mybatis.dynamic.sql.util;
 
-import java.util.function.Supplier;
+import org.jspecify.annotations.Nullable;
 
 public interface Utilities {
-    static <T> T buildIfNecessary(T current, Supplier<T> builder) {
-        return current == null ? builder.get() : current;
-    }
-
-    static long safelyUnbox(Long l) {
+    static long safelyUnbox(@Nullable Long l) {
         return l == null ? 0 : l;
     }
 }
diff --git a/src/main/java/org/mybatis/dynamic/sql/util/Validator.java b/src/main/java/org/mybatis/dynamic/sql/util/Validator.java
index 1d4b3f7b8..7563bfc70 100644
--- a/src/main/java/org/mybatis/dynamic/sql/util/Validator.java
+++ b/src/main/java/org/mybatis/dynamic/sql/util/Validator.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,6 +17,7 @@
 
 import java.util.Collection;
 
+import org.jspecify.annotations.Nullable;
 import org.mybatis.dynamic.sql.exception.InvalidSqlException;
 
 public class Validator {
@@ -26,12 +27,20 @@ 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) {
-        internalAssertFalse(condition, Messages.getString(messageNumber));
+        if (condition) {
+            throw new InvalidSqlException(Messages.getString(messageNumber));
+        }
     }
 
     public static void assertFalse(boolean condition, String messageNumber, String p1) {
-        internalAssertFalse(condition, Messages.getString(messageNumber, p1));
+        if (condition) {
+            throw new InvalidSqlException(Messages.getString(messageNumber, p1));
+        }
     }
 
     public static void assertTrue(boolean condition, String messageNumber) {
@@ -42,9 +51,9 @@ public static void assertTrue(boolean condition, String messageNumber, String p1
         assertFalse(!condition, messageNumber, p1);
     }
 
-    private static void internalAssertFalse(boolean condition, String message) {
-        if (condition) {
-            throw new InvalidSqlException(message);
+    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 9028c9c4e..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,6 +18,7 @@
 import java.util.Objects;
 import java.util.function.Supplier;
 
+import org.jspecify.annotations.Nullable;
 import org.mybatis.dynamic.sql.SqlColumn;
 
 public class ValueMapping<T> extends AbstractColumnMapping {
@@ -32,7 +33,7 @@ private ValueMapping(SqlColumn<T> column, Supplier<T> valueSupplier) {
         localColumn = Objects.requireNonNull(column);
     }
 
-    public Object value() {
+    public @Nullable Object value() {
         return localColumn.convertParameterType(valueSupplier.get());
     }
 
diff --git a/src/main/java/org/mybatis/dynamic/sql/util/ValueOrNullMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/ValueOrNullMapping.java
index 85690db59..f39a706ac 100644
--- a/src/main/java/org/mybatis/dynamic/sql/util/ValueOrNullMapping.java
+++ b/src/main/java/org/mybatis/dynamic/sql/util/ValueOrNullMapping.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,15 +19,16 @@
 import java.util.Optional;
 import java.util.function.Supplier;
 
+import org.jspecify.annotations.Nullable;
 import org.mybatis.dynamic.sql.SqlColumn;
 
 public class ValueOrNullMapping<T> extends AbstractColumnMapping {
 
-    private final Supplier<T> valueSupplier;
+    private final Supplier<@Nullable T> valueSupplier;
     // keep a reference to the column so we don't lose the type
     private final SqlColumn<T> localColumn;
 
-    private ValueOrNullMapping(SqlColumn<T> column, Supplier<T> valueSupplier) {
+    private ValueOrNullMapping(SqlColumn<T> column, Supplier<@Nullable T> valueSupplier) {
         super(column);
         this.valueSupplier = Objects.requireNonNull(valueSupplier);
         localColumn = Objects.requireNonNull(column);
@@ -42,7 +43,7 @@ public <R> R accept(ColumnMappingVisitor<R> visitor) {
         return visitor.visit(this);
     }
 
-    public static <T> ValueOrNullMapping<T> of(SqlColumn<T> column, Supplier<T> valueSupplier) {
+    public static <T> ValueOrNullMapping<T> of(SqlColumn<T> 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
index 5bee7afd5..5420c644e 100644
--- a/src/main/java/org/mybatis/dynamic/sql/util/ValueWhenPresentMapping.java
+++ b/src/main/java/org/mybatis/dynamic/sql/util/ValueWhenPresentMapping.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,26 +19,27 @@
 import java.util.Optional;
 import java.util.function.Supplier;
 
+import org.jspecify.annotations.Nullable;
 import org.mybatis.dynamic.sql.SqlColumn;
 
 public class ValueWhenPresentMapping<T> extends AbstractColumnMapping {
 
-    private final Supplier<T> valueSupplier;
+    private final Supplier<@Nullable T> valueSupplier;
     // keep a reference to the column so we don't lose the type
     private final SqlColumn<T> localColumn;
 
-    private ValueWhenPresentMapping(SqlColumn<T> column, Supplier<T> valueSupplier) {
+    private ValueWhenPresentMapping(SqlColumn<T> column, Supplier<@Nullable T> valueSupplier) {
         super(column);
         this.valueSupplier = Objects.requireNonNull(valueSupplier);
         localColumn = Objects.requireNonNull(column);
     }
 
     public Optional<Object> value() {
-        return Optional.ofNullable(valueSupplier.get()).map(this::convert);
+        return Optional.ofNullable(valueSupplier.get()).flatMap(this::convert);
     }
 
-    private Object convert(T value) {
-        return localColumn.convertParameterType(value);
+    private Optional<Object> convert(T value) {
+        return Optional.ofNullable(localColumn.convertParameterType(value));
     }
 
     @Override
@@ -46,7 +47,7 @@ public <R> R accept(ColumnMappingVisitor<R> visitor) {
         return visitor.visit(this);
     }
 
-    public static <T> ValueWhenPresentMapping<T> of(SqlColumn<T> column, Supplier<T> valueSupplier) {
+    public static <T> ValueWhenPresentMapping<T> of(SqlColumn<T> 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
index f9319ac3e..d0b6c580d 100644
--- a/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonCountMapper.java
+++ b/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonCountMapper.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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
index b8a827fcf..bd7cb06dd 100644
--- a/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonDeleteMapper.java
+++ b/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonDeleteMapper.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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
index 8ec6e889e..4dda5cc11 100644
--- a/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonGeneralInsertMapper.java
+++ b/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonGeneralInsertMapper.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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
index 0a834f604..bd864b610 100644
--- a/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonInsertMapper.java
+++ b/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonInsertMapper.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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
index d7d67edc0..8f0f350ee 100644
--- a/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonSelectMapper.java
+++ b/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonSelectMapper.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -20,9 +20,9 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.function.Function;
-import java.util.stream.Collectors;
 
 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;
 
@@ -50,7 +50,7 @@ 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 (may be aliased if an alias is specified in the select statement). Map entries will be
+     * 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.
@@ -59,7 +59,7 @@ public interface CommonSelectMapper {
      * @return A Map containing the row values.
      */
     @SelectProvider(type = SqlProviderAdapter.class, method = "select")
-    Map<String, Object> selectOneMappedRow(SelectStatementProvider selectStatement);
+    @Nullable Map<String, Object> selectOneMappedRow(SelectStatementProvider selectStatement);
 
     /**
      * Select a single row of values and then convert the values to a custom type. This is similar
@@ -75,16 +75,17 @@ public interface CommonSelectMapper {
      * @param <R> the datatype of the converted object
      * @return the converted object
      */
-    default <R> R selectOne(SelectStatementProvider selectStatement,
+    default <R> @Nullable R selectOne(SelectStatementProvider selectStatement,
                             Function<Map<String, Object>, R> rowMapper) {
-        return rowMapper.apply(selectOneMappedRow(selectStatement));
+        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 (may be aliased if an alias is specified in the select statement). Map entries will be
+     * 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.
@@ -110,7 +111,7 @@ default <R> List<R> selectMany(SelectStatementProvider selectStatement,
                                    Function<Map<String, Object>, R> rowMapper) {
         return selectManyMappedRows(selectStatement).stream()
                 .map(rowMapper)
-                .collect(Collectors.toList());
+                .toList();
     }
 
     /**
@@ -123,7 +124,7 @@ default <R> List<R> selectMany(SelectStatementProvider selectStatement,
      *     column is null
      */
     @SelectProvider(type = SqlProviderAdapter.class, method = "select")
-    BigDecimal selectOneBigDecimal(SelectStatementProvider selectStatement);
+    @Nullable BigDecimal selectOneBigDecimal(SelectStatementProvider selectStatement);
 
     /**
      * Retrieve a single {@link java.math.BigDecimal} from a result set. The result set must have
@@ -158,7 +159,7 @@ default <R> List<R> selectMany(SelectStatementProvider selectStatement,
      *     column is null
      */
     @SelectProvider(type = SqlProviderAdapter.class, method = "select")
-    Double selectOneDouble(SelectStatementProvider selectStatement);
+    @Nullable Double selectOneDouble(SelectStatementProvider selectStatement);
 
     /**
      * Retrieve a single {@link java.lang.Double} from a result set. The result set must have
@@ -193,7 +194,7 @@ default <R> List<R> selectMany(SelectStatementProvider selectStatement,
      *     column is null
      */
     @SelectProvider(type = SqlProviderAdapter.class, method = "select")
-    Integer selectOneInteger(SelectStatementProvider selectStatement);
+    @Nullable Integer selectOneInteger(SelectStatementProvider selectStatement);
 
     /**
      * Retrieve a single {@link java.lang.Integer} from a result set. The result set must have
@@ -228,7 +229,7 @@ default <R> List<R> selectMany(SelectStatementProvider selectStatement,
      *     column is null
      */
     @SelectProvider(type = SqlProviderAdapter.class, method = "select")
-    Long selectOneLong(SelectStatementProvider selectStatement);
+    @Nullable Long selectOneLong(SelectStatementProvider selectStatement);
 
     /**
      * Retrieve a single {@link java.lang.Long} from a result set. The result set must have
@@ -263,7 +264,7 @@ default <R> List<R> selectMany(SelectStatementProvider selectStatement,
      *     column is null
      */
     @SelectProvider(type = SqlProviderAdapter.class, method = "select")
-    String selectOneString(SelectStatementProvider selectStatement);
+    @Nullable String selectOneString(SelectStatementProvider selectStatement);
 
     /**
      * Retrieve a single {@link java.lang.String} from a result set. The result set must have
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
index c7e4e40ce..0f25b575d 100644
--- a/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonUpdateMapper.java
+++ b/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/CommonUpdateMapper.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 5b1906429..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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
index 485868ba6..8672e98ea 100644
--- a/src/main/java/org/mybatis/dynamic/sql/util/spring/BatchInsertUtility.java
+++ b/src/main/java/org/mybatis/dynamic/sql/util/spring/BatchInsertUtility.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -16,7 +16,6 @@
 package org.mybatis.dynamic.sql.util.spring;
 
 import java.util.List;
-import java.util.stream.Collectors;
 
 import org.springframework.jdbc.core.namedparam.SqlParameterSource;
 import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils;
@@ -35,20 +34,10 @@ private BatchInsertUtility() {}
     public static <T> SqlParameterSource[] createBatch(List<T> rows) {
         List<RowHolder<T>> tt = rows.stream()
                 .map(RowHolder::new)
-                .collect(Collectors.toList());
+                .toList();
 
         return SqlParameterSourceUtils.createBatch(tt);
     }
 
-    public static class RowHolder<T> {
-        private final T row;
-
-        public RowHolder(T row) {
-            this.row = row;
-        }
-
-        public T getRow() {
-            return row;
-        }
-    }
+    public record RowHolder<T> (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
index 56fac7d5c..4e630e1d2 100644
--- a/src/main/java/org/mybatis/dynamic/sql/util/spring/NamedParameterJdbcTemplateExtensions.java
+++ b/src/main/java/org/mybatis/dynamic/sql/util/spring/NamedParameterJdbcTemplateExtensions.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 5486b31de..000000000
--- a/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchCursorReaderSelectModel.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- *    Copyright 2016-2024 the original author or authors.
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *       https://www.apache.org/licenses/LICENSE-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 final 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
+ * (<a href="http://www.mybatis.org/spring/">http://www.mybatis.org/spring/</a>).
+ */
+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 648d71218..000000000
--- a/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchPagingReaderSelectModel.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- *    Copyright 2016-2024 the original author or authors.
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *       https://www.apache.org/licenses/LICENSE-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 final 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 final Map<String, Object> parameters = new HashMap<>();
-        private final String selectStatement;
-
-        public LimitAndOffsetDecorator(SelectStatementProvider delegate) {
-            parameters.putAll(delegate.getParameters());
-
-            selectStatement = delegate.getSelectStatement()
-                    + " LIMIT #{_pagesize} OFFSET #{_skiprows}"; //$NON-NLS-1$
-        }
-
-        @Override
-        public Map<String, Object> 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 f212a0d04..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,13 +17,9 @@
 
 import java.util.Map;
 
-import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
-
 public class SpringBatchProviderAdapter {
 
     public String select(Map<String, Object> 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/SpringBatchReaderRenderingStrategy.java b/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchReaderRenderingStrategy.java
deleted file mode 100644
index 48b1b9b8c..000000000
--- a/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchReaderRenderingStrategy.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- *    Copyright 2016-2024 the original author or authors.
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *       https://www.apache.org/licenses/LICENSE-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.BindableColumn;
-import org.mybatis.dynamic.sql.render.MyBatis3RenderingStrategy;
-
-/**
- * This rendering strategy should be used for MyBatis3 statements using one of the
- * Spring batch readers supplied by mybatis-spring integration (http://www.mybatis.org/spring/).
- * Those readers are MyBatisPagingItemReader and MyBatisCursorItemReader.
- *
- */
-public class SpringBatchReaderRenderingStrategy extends MyBatis3RenderingStrategy {
-
-    @Override
-    public String getFormattedJdbcPlaceholder(BindableColumn<?> column, String prefix, String parameterName) {
-        String newPrefix = SpringBatchUtility.PARAMETER_KEY + "." + prefix; //$NON-NLS-1$
-        return super.getFormattedJdbcPlaceholder(column, newPrefix, parameterName);
-    }
-}
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 63b64429a..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -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<String, Object> toParameterValues(SelectStatementProvider selectStatement) {
-        Map<String, Object> 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.
+     * 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.
      *
-     * <p><b>Important</b> 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.
+     * <p>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.
      *
-     * @param selectList a column list for the SELECT statement
-     * @return FromGatherer used to continue a SELECT statement
+     * <p>See <a href="https://mybatis.org/spring/batch.html">https://mybatis.org/spring/batch.html</a> for details.
      */
-    public static QueryExpressionDSL.FromGatherer<SpringBatchPagingReaderSelectModel> 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.
+     * 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.
      *
-     * @param selectList a column list for the SELECT statement
-     * @return FromGatherer used to continue a SELECT statement
+     * <p>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.
+     *
+     * <p>See <a href="https://mybatis.org/spring/batch.html">https://mybatis.org/spring/batch.html</a> for details.
      */
-    public static QueryExpressionDSL.FromGatherer<SpringBatchCursorReaderSelectModel> 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<String, Object> toParameterValues(SelectStatementProvider selectStatement) {
+        var parameterValues = new HashMap<String, Object>();
+        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/AbstractWhereFinisher.java b/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereFinisher.java
index edad1cccc..260fef7ea 100644
--- a/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereFinisher.java
+++ b/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereFinisher.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,6 +19,8 @@
 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;
@@ -37,11 +39,12 @@ void initialize(SqlCriterion sqlCriterion) {
         setInitialCriterion(sqlCriterion, StatementType.WHERE);
     }
 
-    void initialize(SqlCriterion sqlCriterion, List<AndOrCriteriaGroup> subCriteria) {
+    void initialize(@Nullable SqlCriterion sqlCriterion, List<AndOrCriteriaGroup> subCriteria) {
         setInitialCriterion(sqlCriterion, StatementType.WHERE);
         super.subCriteria.addAll(subCriteria);
     }
 
+    @NonNull
     @Override
     public T configureStatement(Consumer<StatementConfiguration> consumer) {
         parentStatement.configureStatement(consumer);
diff --git a/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereStarter.java b/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereStarter.java
index 03da51bd8..17efa091e 100644
--- a/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereStarter.java
+++ b/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereStarter.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,14 +18,15 @@
 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.VisitableCondition;
 import org.mybatis.dynamic.sql.util.ConfigurableStatement;
 
 /**
@@ -35,14 +36,14 @@
  *
  * @param <F> the implementation of the Where DSL customized for a particular SQL statement.
  */
-public abstract class AbstractWhereStarter<F extends AbstractWhereFinisher<?>, D extends AbstractWhereStarter<F, D>>
-        implements ConfigurableStatement<D> {
+public interface AbstractWhereStarter<F extends AbstractWhereFinisher<?>, D extends AbstractWhereStarter<F, D>>
+        extends ConfigurableStatement<D> {
 
-    public <T> F where(BindableColumn<T> column, VisitableCondition<T> condition, AndOrCriteriaGroup... subCriteria) {
+    default <T> F where(BindableColumn<T> column, RenderableCondition<T> condition, AndOrCriteriaGroup... subCriteria) {
         return where(column, condition, Arrays.asList(subCriteria));
     }
 
-    public <T> F where(BindableColumn<T> column, VisitableCondition<T> condition,
+    default <T> F where(BindableColumn<T> column, RenderableCondition<T> condition,
                        List<AndOrCriteriaGroup> subCriteria) {
         SqlCriterion sqlCriterion = ColumnAndConditionCriterion.withColumn(column)
                 .withCondition(condition)
@@ -52,11 +53,11 @@ public <T> F where(BindableColumn<T> column, VisitableCondition<T> condition,
         return initialize(sqlCriterion);
     }
 
-    public F where(ExistsPredicate existsPredicate, AndOrCriteriaGroup... subCriteria) {
+    default F where(ExistsPredicate existsPredicate, AndOrCriteriaGroup... subCriteria) {
         return where(existsPredicate, Arrays.asList(subCriteria));
     }
 
-    public F where(ExistsPredicate existsPredicate, List<AndOrCriteriaGroup> subCriteria) {
+    default F where(ExistsPredicate existsPredicate, List<AndOrCriteriaGroup> subCriteria) {
         ExistsCriterion sqlCriterion = new ExistsCriterion.Builder()
                 .withExistsPredicate(existsPredicate)
                 .withSubCriteria(subCriteria)
@@ -65,11 +66,11 @@ public F where(ExistsPredicate existsPredicate, List<AndOrCriteriaGroup> subCrit
         return initialize(sqlCriterion);
     }
 
-    public F where(SqlCriterion initialCriterion, AndOrCriteriaGroup... subCriteria) {
+    default F where(SqlCriterion initialCriterion, AndOrCriteriaGroup... subCriteria) {
         return where(initialCriterion, Arrays.asList(subCriteria));
     }
 
-    public F where(SqlCriterion initialCriterion, List<AndOrCriteriaGroup> subCriteria) {
+    default F where(@Nullable SqlCriterion initialCriterion, List<AndOrCriteriaGroup> subCriteria) {
         SqlCriterion sqlCriterion = new CriteriaGroup.Builder()
                 .withInitialCriterion(initialCriterion)
                 .withSubCriteria(subCriteria)
@@ -78,7 +79,7 @@ public F where(SqlCriterion initialCriterion, List<AndOrCriteriaGroup> subCriter
         return initialize(sqlCriterion);
     }
 
-    public F where(List<AndOrCriteriaGroup> subCriteria) {
+    default F where(List<AndOrCriteriaGroup> subCriteria) {
         SqlCriterion sqlCriterion = new CriteriaGroup.Builder()
                 .withSubCriteria(subCriteria)
                 .build();
@@ -86,9 +87,9 @@ public F where(List<AndOrCriteriaGroup> subCriteria) {
         return initialize(sqlCriterion);
     }
 
-    public abstract F where();
+    F where();
 
-    public F applyWhere(WhereApplier whereApplier) {
+    default F applyWhere(WhereApplier whereApplier) {
         F finisher = where();
         whereApplier.accept(finisher);
         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
index acede2590..7cd42e73e 100644
--- a/src/main/java/org/mybatis/dynamic/sql/where/EmbeddedWhereModel.java
+++ b/src/main/java/org/mybatis/dynamic/sql/where/EmbeddedWhereModel.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 ce2289695..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 0eb23f638..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,7 +17,6 @@
 
 import java.util.function.Consumer;
 
-import org.jetbrains.annotations.NotNull;
 import org.mybatis.dynamic.sql.configuration.StatementConfiguration;
 import org.mybatis.dynamic.sql.util.Buildable;
 
@@ -26,7 +25,7 @@
  *
  *  <p>This can also be used to create reusable where clauses for different statements.
  */
-public class WhereDSL extends AbstractWhereStarter<WhereDSL.StandaloneWhereFinisher, WhereDSL> {
+public class WhereDSL implements AbstractWhereStarter<WhereDSL.StandaloneWhereFinisher, WhereDSL> {
     private final StatementConfiguration statementConfiguration = new StatementConfiguration();
     private final StandaloneWhereFinisher whereBuilder = new StandaloneWhereFinisher();
 
@@ -52,7 +51,6 @@ protected StandaloneWhereFinisher getThis() {
             return this;
         }
 
-        @NotNull
         @Override
         public WhereModel build() {
             return new WhereModel.Builder()
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 8db761223..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,12 +18,14 @@
 import java.util.Objects;
 import java.util.Optional;
 
+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;
 
@@ -92,13 +94,13 @@ private Optional<WhereClauseProvider> render(RenderingContext renderingContext)
     }
 
     private WhereClauseProvider toWhereClauseProvider(FragmentAndParameters fragmentAndParameters) {
-        return WhereClauseProvider.withWhereClause(fragmentAndParameters.fragment())
+        return DefaultWhereClauseProvider.withWhereClause(fragmentAndParameters.fragment())
                 .withParameters(fragmentAndParameters.parameters())
                 .build();
     }
 
     public static class Builder extends AbstractBuilder<Builder> {
-        private StatementConfiguration statementConfiguration;
+        private @Nullable StatementConfiguration statementConfiguration;
 
         public Builder withStatementConfiguration(StatementConfiguration statementConfiguration) {
             this.statementConfiguration = statementConfiguration;
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 ddd272f40..c9514f3fa 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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,6 +17,8 @@
 
 import java.util.function.Supplier;
 
+import org.jspecify.annotations.NonNull;
+
 /**
  * Utility class supporting the "and" part of a between condition. This class supports builders, so it is mutable.
  *
@@ -29,20 +31,18 @@
  */
 public abstract class AndGatherer<T, R> {
     protected final T value1;
-    protected T value2;
 
     protected AndGatherer(T value1) {
         this.value1 = value1;
     }
 
     public R and(T value2) {
-        this.value2 = value2;
-        return build();
+        return build(value2);
     }
 
-    public R and(Supplier<T> valueSupplier2) {
+    public R and(Supplier<@NonNull T> valueSupplier2) {
         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 <T>
+ *            the type of field for the between condition
+ * @param <R>
+ *            the type of condition being built
+ */
+public abstract class AndWhenPresentGatherer<T, R> {
+    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<T> extends RenderableCondition<T> {
+
+    @Override
+    default FragmentAndParameters renderLeftColumn(RenderingContext renderingContext,
+                                                   BindableColumn<T> 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 ff21433d0..0f7fcd66a 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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,15 +15,27 @@
  */
 package org.mybatis.dynamic.sql.where.condition;
 
-import java.util.Objects;
+import java.util.NoSuchElementException;
 import java.util.function.BiPredicate;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
+import org.jspecify.annotations.NonNull;
 import org.mybatis.dynamic.sql.AbstractTwoValueCondition;
 
-public class IsBetween<T> extends AbstractTwoValueCondition<T> {
-    private static final IsBetween<?> EMPTY = new IsBetween<Object>(null, null) {
+public class IsBetween<T> extends AbstractTwoValueCondition<@NonNull T>
+        implements AbstractTwoValueCondition.Filterable<T>, AbstractTwoValueCondition.Mappable<T> {
+    private static final IsBetween<?> EMPTY = new IsBetween<Object>(-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;
@@ -51,39 +63,23 @@ public String operator2() {
     }
 
     @Override
-    public IsBetween<T> filter(BiPredicate<? super T, ? super T> predicate) {
+    public IsBetween<T> filter(BiPredicate<? super @NonNull T, ? super @NonNull T> predicate) {
         return filterSupport(predicate, IsBetween::empty, this);
     }
 
     @Override
-    public IsBetween<T> filter(Predicate<? super T> predicate) {
+    public IsBetween<T> filter(Predicate<? super @NonNull T> predicate) {
         return filterSupport(predicate, IsBetween::empty, this);
     }
 
-    /**
-     * 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 <R> 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.
-     */
-    public <R> IsBetween<R> map(Function<? super T, ? extends R> mapper1, Function<? super T, ? extends R> mapper2) {
+    @Override
+    public <R> IsBetween<R> map(Function<? super @NonNull T, ? extends @NonNull R> mapper1,
+                                Function<? super @NonNull T, ? extends @NonNull R> mapper2) {
         return mapSupport(mapper1, mapper2, IsBetween::new, IsBetween::empty);
     }
 
-    /**
-     * 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 <R> 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.
-     */
-    public <R> IsBetween<R> map(Function<? super T, ? extends R> mapper) {
+    @Override
+    public <R> IsBetween<R> map(Function<? super @NonNull T, ? extends @NonNull R> mapper) {
         return map(mapper, mapper);
     }
 
@@ -91,29 +87,14 @@ public static <T> Builder<T> isBetween(T value1) {
         return new Builder<>(value1);
     }
 
-    public static <T> WhenPresentBuilder<T> isBetweenWhenPresent(T value1) {
-        return new WhenPresentBuilder<>(value1);
-    }
-
     public static class Builder<T> extends AndGatherer<T, IsBetween<T>> {
         private Builder(T value1) {
             super(value1);
         }
 
         @Override
-        protected IsBetween<T> build() {
+        protected IsBetween<T> build(T value2) {
             return new IsBetween<>(value1, value2);
         }
     }
-
-    public static class WhenPresentBuilder<T> extends AndGatherer<T, IsBetween<T>> {
-        private WhenPresentBuilder(T value1) {
-            super(value1);
-        }
-
-        @Override
-        protected IsBetween<T> build() {
-            return new IsBetween<>(value1, value2).filter(Objects::nonNull);
-        }
-    }
 }
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
new file mode 100644
index 000000000..bc9c12d37
--- /dev/null
+++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsBetweenWhenPresent.java
@@ -0,0 +1,109 @@
+/*
+ *    Copyright 2016-2025 the original author or authors.
+ *
+ *    Licensed under the Apache License, Version 2.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.NoSuchElementException;
+import java.util.function.BiPredicate;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+import org.mybatis.dynamic.sql.AbstractTwoValueCondition;
+
+public class IsBetweenWhenPresent<T> extends AbstractTwoValueCondition<T>
+        implements AbstractTwoValueCondition.Filterable<T>, AbstractTwoValueCondition.Mappable<T> {
+    private static final IsBetweenWhenPresent<?> EMPTY = new IsBetweenWhenPresent<Object>(-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;
+        }
+    };
+
+    public static <T> IsBetweenWhenPresent<T> empty() {
+        @SuppressWarnings("unchecked")
+        IsBetweenWhenPresent<T> t = (IsBetweenWhenPresent<T>) EMPTY;
+        return t;
+    }
+
+    protected IsBetweenWhenPresent(T value1, T value2) {
+        super(value1, value2);
+    }
+
+    @Override
+    public String operator1() {
+        return "between"; //$NON-NLS-1$
+    }
+
+    @Override
+    public String operator2() {
+        return "and"; //$NON-NLS-1$
+    }
+
+    @Override
+    public IsBetweenWhenPresent<T> filter(BiPredicate<? super @NonNull T, ? super @NonNull T> predicate) {
+        return filterSupport(predicate, IsBetweenWhenPresent::empty, this);
+    }
+
+    @Override
+    public IsBetweenWhenPresent<T> filter(Predicate<? super @NonNull T> predicate) {
+        return filterSupport(predicate, IsBetweenWhenPresent::empty, this);
+    }
+
+    @Override
+    public <R> IsBetweenWhenPresent<R> map(Function<? super @NonNull T, ? extends @Nullable R> mapper1,
+                                           Function<? super @NonNull T, ? extends @Nullable R> mapper2) {
+        return mapSupport(mapper1, mapper2, IsBetweenWhenPresent::of, IsBetweenWhenPresent::empty);
+    }
+
+    @Override
+    public <R> IsBetweenWhenPresent<R> map(Function<? super @NonNull T, ? extends @Nullable R> mapper) {
+        return map(mapper, mapper);
+    }
+
+    public static <T> IsBetweenWhenPresent<T> of(@Nullable T value1, @Nullable T value2) {
+        if (value1 == null || value2 == null) {
+            return empty();
+        } else {
+            return new IsBetweenWhenPresent<>(value1, value2);
+        }
+    }
+
+    public static <T> Builder<T> isBetweenWhenPresent(@Nullable T value1) {
+        return new Builder<>(value1);
+    }
+
+    public static class Builder<T> extends AndWhenPresentGatherer<T, IsBetweenWhenPresent<T>> {
+        private Builder(@Nullable T value1) {
+            super(value1);
+        }
+
+        @Override
+        protected IsBetweenWhenPresent<T> 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 2682dd602..db8548f61 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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,14 +15,22 @@
  */
 package org.mybatis.dynamic.sql.where.condition;
 
+import java.util.NoSuchElementException;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
+import org.jspecify.annotations.NonNull;
 import org.mybatis.dynamic.sql.AbstractSingleValueCondition;
 
-public class IsEqualTo<T> extends AbstractSingleValueCondition<T> {
+public class IsEqualTo<T> extends AbstractSingleValueCondition<T>
+        implements AbstractSingleValueCondition.Filterable<T>, AbstractSingleValueCondition.Mappable<T> {
+
+    private static final IsEqualTo<?> EMPTY = new IsEqualTo<Object>(-1) {
+        @Override
+        public Object value() {
+            throw new NoSuchElementException("No value present"); //$NON-NLS-1$
+        }
 
-    private static final IsEqualTo<?> EMPTY = new IsEqualTo<Object>(null) {
         @Override
         public boolean isEmpty() {
             return true;
@@ -49,20 +57,12 @@ public static <T> IsEqualTo<T> of(T value) {
     }
 
     @Override
-    public IsEqualTo<T> filter(Predicate<? super T> predicate) {
+    public IsEqualTo<T> filter(Predicate<? super @NonNull T> predicate) {
         return filterSupport(predicate, IsEqualTo::empty, this);
     }
 
-    /**
-     * 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 <R> 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.
-     */
-    public <R> IsEqualTo<R> map(Function<? super T, ? extends R> mapper) {
+    @Override
+    public <R> IsEqualTo<R> map(Function<? super @NonNull T, ? extends @NonNull R> 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 80fa86094..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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
new file mode 100644
index 000000000..2dd8c746d
--- /dev/null
+++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsEqualToWhenPresent.java
@@ -0,0 +1,73 @@
+/*
+ *    Copyright 2016-2025 the original author or authors.
+ *
+ *    Licensed under the Apache License, Version 2.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.NoSuchElementException;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+import org.mybatis.dynamic.sql.AbstractSingleValueCondition;
+
+public class IsEqualToWhenPresent<T> extends AbstractSingleValueCondition<T>
+        implements AbstractSingleValueCondition.Filterable<T>, AbstractSingleValueCondition.Mappable<T> {
+
+    private static final IsEqualToWhenPresent<?> EMPTY = new IsEqualToWhenPresent<Object>(-1) {
+        @Override
+        public Object value() {
+            throw new NoSuchElementException("No value present"); //$NON-NLS-1$
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return true;
+        }
+    };
+
+    public static <T> IsEqualToWhenPresent<T> empty() {
+        @SuppressWarnings("unchecked")
+        IsEqualToWhenPresent<T> t = (IsEqualToWhenPresent<T>) EMPTY;
+        return t;
+    }
+
+    protected IsEqualToWhenPresent(T value) {
+        super(value);
+    }
+
+    @Override
+    public String operator() {
+        return "="; //$NON-NLS-1$
+    }
+
+    public static <T> IsEqualToWhenPresent<T> of(@Nullable T value) {
+        if (value == null) {
+            return empty();
+        } else {
+            return new IsEqualToWhenPresent<>(value);
+        }
+    }
+
+    @Override
+    public IsEqualToWhenPresent<T> filter(Predicate<? super @NonNull T> predicate) {
+        return filterSupport(predicate, IsEqualToWhenPresent::empty, this);
+    }
+
+    @Override
+    public <R> IsEqualToWhenPresent<R> map(Function<? super @NonNull T, ? extends @Nullable R> 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 6b4def2ce..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,7 +15,6 @@
  */
 package org.mybatis.dynamic.sql.where.condition;
 
-import org.jetbrains.annotations.NotNull;
 import org.mybatis.dynamic.sql.AbstractSubselectCondition;
 import org.mybatis.dynamic.sql.select.SelectModel;
 import org.mybatis.dynamic.sql.util.Buildable;
@@ -26,7 +25,6 @@ protected IsEqualToWithSubselect(Buildable<SelectModel> selectModelBuilder) {
         super(selectModelBuilder);
     }
 
-    @NotNull
     public static <T> IsEqualToWithSubselect<T> of(Buildable<SelectModel> selectModelBuilder) {
         return new IsEqualToWithSubselect<>(selectModelBuilder);
     }
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 3e45574ad..577a400f2 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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,13 +15,21 @@
  */
 package org.mybatis.dynamic.sql.where.condition;
 
+import java.util.NoSuchElementException;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
+import org.jspecify.annotations.NonNull;
 import org.mybatis.dynamic.sql.AbstractSingleValueCondition;
 
-public class IsGreaterThan<T> extends AbstractSingleValueCondition<T> {
-    private static final IsGreaterThan<?> EMPTY = new IsGreaterThan<Object>(null) {
+public class IsGreaterThan<T> extends AbstractSingleValueCondition<T>
+        implements AbstractSingleValueCondition.Filterable<T>, AbstractSingleValueCondition.Mappable<T> {
+    private static final IsGreaterThan<?> EMPTY = new IsGreaterThan<Object>(-1) {
+        @Override
+        public Object value() {
+            throw new NoSuchElementException("No value present"); //$NON-NLS-1$
+        }
+
         @Override
         public boolean isEmpty() {
             return true;
@@ -48,20 +56,12 @@ public static <T> IsGreaterThan<T> of(T value) {
     }
 
     @Override
-    public IsGreaterThan<T> filter(Predicate<? super T> predicate) {
+    public IsGreaterThan<T> filter(Predicate<? super @NonNull T> predicate) {
         return filterSupport(predicate, IsGreaterThan::empty, this);
     }
 
-    /**
-     * 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 <R> 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.
-     */
-    public <R> IsGreaterThan<R> map(Function<? super T, ? extends R> mapper) {
+    @Override
+    public <R> IsGreaterThan<R> map(Function<? super @NonNull T, ? extends @NonNull R> 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 dc0bb513e..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 8212a3b5b..5fb4bd0d4 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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,13 +15,21 @@
  */
 package org.mybatis.dynamic.sql.where.condition;
 
+import java.util.NoSuchElementException;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
+import org.jspecify.annotations.NonNull;
 import org.mybatis.dynamic.sql.AbstractSingleValueCondition;
 
-public class IsGreaterThanOrEqualTo<T> extends AbstractSingleValueCondition<T> {
-    private static final IsGreaterThanOrEqualTo<?> EMPTY = new IsGreaterThanOrEqualTo<Object>(null) {
+public class IsGreaterThanOrEqualTo<T> extends AbstractSingleValueCondition<T>
+        implements AbstractSingleValueCondition.Filterable<T>, AbstractSingleValueCondition.Mappable<T> {
+    private static final IsGreaterThanOrEqualTo<?> EMPTY = new IsGreaterThanOrEqualTo<Object>(-1) {
+        @Override
+        public Object value() {
+            throw new NoSuchElementException("No value present"); //$NON-NLS-1$
+        }
+
         @Override
         public boolean isEmpty() {
             return true;
@@ -48,20 +56,12 @@ public static <T> IsGreaterThanOrEqualTo<T> of(T value) {
     }
 
     @Override
-    public IsGreaterThanOrEqualTo<T> filter(Predicate<? super T> predicate) {
+    public IsGreaterThanOrEqualTo<T> filter(Predicate<? super @NonNull T> predicate) {
         return filterSupport(predicate, IsGreaterThanOrEqualTo::empty, this);
     }
 
-    /**
-     * 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 <R> 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.
-     */
-    public <R> IsGreaterThanOrEqualTo<R> map(Function<? super T, ? extends R> mapper) {
+    @Override
+    public <R> IsGreaterThanOrEqualTo<R> map(Function<? super @NonNull T, ? extends @NonNull R> 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 413d0b206..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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
new file mode 100644
index 000000000..01f895dc9
--- /dev/null
+++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanOrEqualToWhenPresent.java
@@ -0,0 +1,73 @@
+/*
+ *    Copyright 2016-2025 the original author or authors.
+ *
+ *    Licensed under the Apache License, Version 2.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.NoSuchElementException;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+import org.mybatis.dynamic.sql.AbstractSingleValueCondition;
+
+public class IsGreaterThanOrEqualToWhenPresent<T> extends AbstractSingleValueCondition<T>
+        implements AbstractSingleValueCondition.Filterable<T>, AbstractSingleValueCondition.Mappable<T> {
+    private static final IsGreaterThanOrEqualToWhenPresent<?> EMPTY =
+            new IsGreaterThanOrEqualToWhenPresent<Object>(-1) {
+        @Override
+        public Object value() {
+            throw new NoSuchElementException("No value present"); //$NON-NLS-1$
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return true;
+        }
+    };
+
+    public static <T> IsGreaterThanOrEqualToWhenPresent<T> empty() {
+        @SuppressWarnings("unchecked")
+        IsGreaterThanOrEqualToWhenPresent<T> t = (IsGreaterThanOrEqualToWhenPresent<T>) EMPTY;
+        return t;
+    }
+
+    protected IsGreaterThanOrEqualToWhenPresent(T value) {
+        super(value);
+    }
+
+    @Override
+    public String operator() {
+        return ">="; //$NON-NLS-1$
+    }
+
+    public static <T> IsGreaterThanOrEqualToWhenPresent<T> of(@Nullable T value) {
+        if (value == null) {
+            return empty();
+        } else {
+            return new IsGreaterThanOrEqualToWhenPresent<>(value);
+        }
+    }
+
+    @Override
+    public IsGreaterThanOrEqualToWhenPresent<T> filter(Predicate<? super @NonNull T> predicate) {
+        return filterSupport(predicate, IsGreaterThanOrEqualToWhenPresent::empty, this);
+    }
+
+    @Override
+    public <R> IsGreaterThanOrEqualToWhenPresent<R> map(Function<? super @NonNull T, ? extends @Nullable R> 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 9a7e9385f..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,7 +15,6 @@
  */
 package org.mybatis.dynamic.sql.where.condition;
 
-import org.jetbrains.annotations.NotNull;
 import org.mybatis.dynamic.sql.AbstractSubselectCondition;
 import org.mybatis.dynamic.sql.select.SelectModel;
 import org.mybatis.dynamic.sql.util.Buildable;
@@ -26,7 +25,6 @@ protected IsGreaterThanOrEqualToWithSubselect(Buildable<SelectModel> selectModel
         super(selectModelBuilder);
     }
 
-    @NotNull
     public static <T> IsGreaterThanOrEqualToWithSubselect<T> of(Buildable<SelectModel> selectModelBuilder) {
         return new IsGreaterThanOrEqualToWithSubselect<>(selectModelBuilder);
     }
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
new file mode 100644
index 000000000..779a15596
--- /dev/null
+++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanWhenPresent.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.where.condition;
+
+import java.util.NoSuchElementException;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+import org.mybatis.dynamic.sql.AbstractSingleValueCondition;
+
+public class IsGreaterThanWhenPresent<T> extends AbstractSingleValueCondition<T>
+        implements AbstractSingleValueCondition.Filterable<T>, AbstractSingleValueCondition.Mappable<T> {
+    private static final IsGreaterThanWhenPresent<?> EMPTY = new IsGreaterThanWhenPresent<Object>(-1) {
+        @Override
+        public Object value() {
+            throw new NoSuchElementException("No value present"); //$NON-NLS-1$
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return true;
+        }
+    };
+
+    public static <T> IsGreaterThanWhenPresent<T> empty() {
+        @SuppressWarnings("unchecked")
+        IsGreaterThanWhenPresent<T> t = (IsGreaterThanWhenPresent<T>) EMPTY;
+        return t;
+    }
+
+    protected IsGreaterThanWhenPresent(T value) {
+        super(value);
+    }
+
+    @Override
+    public String operator() {
+        return ">"; //$NON-NLS-1$
+    }
+
+    public static <T> IsGreaterThanWhenPresent<T> of(@Nullable T value) {
+        if (value == null) {
+            return empty();
+        } else {
+            return new IsGreaterThanWhenPresent<>(value);
+        }
+    }
+
+    @Override
+    public IsGreaterThanWhenPresent<T> filter(Predicate<? super @NonNull T> predicate) {
+        return filterSupport(predicate, IsGreaterThanWhenPresent::empty, this);
+    }
+
+    @Override
+    public <R> IsGreaterThanWhenPresent<R> map(Function<? super @NonNull T, ? extends @Nullable R> 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 9f5ff92dc..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,7 +15,6 @@
  */
 package org.mybatis.dynamic.sql.where.condition;
 
-import org.jetbrains.annotations.NotNull;
 import org.mybatis.dynamic.sql.AbstractSubselectCondition;
 import org.mybatis.dynamic.sql.select.SelectModel;
 import org.mybatis.dynamic.sql.util.Buildable;
@@ -26,7 +25,6 @@ protected IsGreaterThanWithSubselect(Buildable<SelectModel> selectModelBuilder)
         super(selectModelBuilder);
     }
 
-    @NotNull
     public static <T> IsGreaterThanWithSubselect<T> of(Buildable<SelectModel> selectModelBuilder) {
         return new IsGreaterThanWithSubselect<>(selectModelBuilder);
     }
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 ce00047b5..67072dc8a 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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -21,10 +21,13 @@
 import java.util.function.Function;
 import java.util.function.Predicate;
 
+import org.jspecify.annotations.NonNull;
 import org.mybatis.dynamic.sql.AbstractListValueCondition;
 import org.mybatis.dynamic.sql.render.RenderingContext;
+import org.mybatis.dynamic.sql.util.Validator;
 
-public class IsIn<T> extends AbstractListValueCondition<T> {
+public class IsIn<T> extends AbstractListValueCondition<T>
+        implements AbstractListValueCondition.Filterable<T>, AbstractListValueCondition.Mappable<T> {
     private static final IsIn<?> EMPTY = new IsIn<>(Collections.emptyList());
 
     public static <T> IsIn<T> empty() {
@@ -39,6 +42,7 @@ protected IsIn(Collection<T> values) {
 
     @Override
     public boolean shouldRender(RenderingContext renderingContext) {
+        Validator.assertNotEmpty(values, "ERROR.44", "IsIn"); //$NON-NLS-1$ //$NON-NLS-2$
         return true;
     }
 
@@ -48,21 +52,13 @@ public String operator() {
     }
 
     @Override
-    public IsIn<T> filter(Predicate<? super T> predicate) {
+    public IsIn<T> filter(Predicate<? super @NonNull T> predicate) {
         return filterSupport(predicate, IsIn::new, this, IsIn::empty);
     }
 
-    /**
-     * If not empty, apply the mapping to each value in the list return a new condition with the mapped values.
-     *     Else return an empty condition (this).
-     *
-     * @param mapper a mapping function to apply to the values, if not empty
-     * @param <R> type of the new condition
-     * @return a new condition with mapped values if renderable, otherwise an empty condition
-     */
-    public <R> IsIn<R> map(Function<? super T, ? extends R> mapper) {
-        Function<Collection<R>, IsIn<R>> constructor = IsIn::new;
-        return mapSupport(mapper, constructor, IsIn::empty);
+    @Override
+    public <R> IsIn<R> map(Function<? super @NonNull T, ? extends @NonNull R> mapper) {
+        return mapSupport(mapper, IsIn::new, IsIn::empty);
     }
 
     @SafeVarargs
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 3fe30c1ef..d26a9c30f 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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,27 +18,33 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.function.Function;
 import java.util.function.Predicate;
-import java.util.function.UnaryOperator;
 
+import org.jspecify.annotations.NonNull;
 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<String>
-        implements CaseInsensitiveVisitableCondition {
-    private static final IsInCaseInsensitive EMPTY = new IsInCaseInsensitive(Collections.emptyList());
+public class IsInCaseInsensitive<T> extends AbstractListValueCondition<T>
+        implements CaseInsensitiveRenderableCondition<T>, AbstractListValueCondition.Filterable<T>,
+        AbstractListValueCondition.Mappable<T> {
+    private static final IsInCaseInsensitive<?> EMPTY = new IsInCaseInsensitive<>(Collections.emptyList());
 
-    public static IsInCaseInsensitive empty() {
-        return EMPTY;
+    public static <T> IsInCaseInsensitive<T> empty() {
+        @SuppressWarnings("unchecked")
+        IsInCaseInsensitive<T> t = (IsInCaseInsensitive<T>) EMPTY;
+        return t;
     }
 
-    protected IsInCaseInsensitive(Collection<String> values) {
-        super(values);
+    protected IsInCaseInsensitive(Collection<T> values) {
+        super(values.stream().map(StringUtilities::upperCaseIfPossible).toList());
     }
 
     @Override
     public boolean shouldRender(RenderingContext renderingContext) {
+        Validator.assertNotEmpty(values, "ERROR.44", "IsInCaseInsensitive"); //$NON-NLS-1$ //$NON-NLS-2$
         return true;
     }
 
@@ -48,26 +54,21 @@ public String operator() {
     }
 
     @Override
-    public IsInCaseInsensitive filter(Predicate<? super String> predicate) {
+    public IsInCaseInsensitive<T> filter(Predicate<? super @NonNull T> predicate) {
         return filterSupport(predicate, IsInCaseInsensitive::new, this, IsInCaseInsensitive::empty);
     }
 
-    /**
-     * If not empty, apply the mapping to each value in the list return a new condition with the mapped values.
-     *     Else return an empty condition (this).
-     *
-     * @param mapper a mapping function to apply to the values, if not empty
-     * @return a new condition with mapped values if renderable, otherwise an empty condition
-     */
-    public IsInCaseInsensitive map(UnaryOperator<String> mapper) {
+    @Override
+    public <R> IsInCaseInsensitive<R> map(Function<? super @NonNull T, ? extends @NonNull R> mapper) {
         return mapSupport(mapper, IsInCaseInsensitive::new, IsInCaseInsensitive::empty);
     }
 
-    public static IsInCaseInsensitive of(String... values) {
+    @SafeVarargs
+    public static <T> IsInCaseInsensitive<T> of(T... values) {
         return of(Arrays.asList(values));
     }
 
-    public static IsInCaseInsensitive of(Collection<String> values) {
-        return new IsInCaseInsensitive(values).map(StringUtilities::safelyUpperCase);
+    public static <T> IsInCaseInsensitive<T> of(Collection<T> 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 b123d30c5..6b4c1e1cd 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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,23 +19,28 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Objects;
+import java.util.function.Function;
 import java.util.function.Predicate;
-import java.util.function.UnaryOperator;
-import java.util.stream.Collectors;
 
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
 import org.mybatis.dynamic.sql.AbstractListValueCondition;
 import org.mybatis.dynamic.sql.util.StringUtilities;
 
-public class IsInCaseInsensitiveWhenPresent extends AbstractListValueCondition<String>
-        implements CaseInsensitiveVisitableCondition {
-    private static final IsInCaseInsensitiveWhenPresent EMPTY = new IsInCaseInsensitiveWhenPresent(Collections.emptyList());
+public class IsInCaseInsensitiveWhenPresent<T> extends AbstractListValueCondition<T>
+        implements CaseInsensitiveRenderableCondition<T>, AbstractListValueCondition.Filterable<T>,
+        AbstractListValueCondition.Mappable<T> {
+    private static final IsInCaseInsensitiveWhenPresent<?> EMPTY =
+            new IsInCaseInsensitiveWhenPresent<>(Collections.emptyList());
 
-    public static IsInCaseInsensitiveWhenPresent empty() {
-        return EMPTY;
+    public static <T> IsInCaseInsensitiveWhenPresent<T> empty() {
+        @SuppressWarnings("unchecked")
+        IsInCaseInsensitiveWhenPresent<T> t = (IsInCaseInsensitiveWhenPresent<T>) EMPTY;
+        return t;
     }
 
-    protected IsInCaseInsensitiveWhenPresent(Collection<String> values) {
-        super(values.stream().filter(Objects::nonNull).collect(Collectors.toList()));
+    protected IsInCaseInsensitiveWhenPresent(Collection<T> values) {
+        super(values.stream().filter(Objects::nonNull).map(StringUtilities::upperCaseIfPossible).toList());
     }
 
     @Override
@@ -44,26 +49,26 @@ public String operator() {
     }
 
     @Override
-    public IsInCaseInsensitiveWhenPresent filter(Predicate<? super String> predicate) {
-        return filterSupport(predicate, IsInCaseInsensitiveWhenPresent::new, this, IsInCaseInsensitiveWhenPresent::empty);
+    public IsInCaseInsensitiveWhenPresent<T> filter(Predicate<? super @NonNull T> predicate) {
+        return filterSupport(predicate, IsInCaseInsensitiveWhenPresent::new, this,
+                IsInCaseInsensitiveWhenPresent::empty);
     }
 
-    /**
-     * If not empty, apply the mapping to each value in the list return a new condition with the mapped values.
-     *     Else return an empty condition (this).
-     *
-     * @param mapper a mapping function to apply to the values, if not empty
-     * @return a new condition with mapped values if renderable, otherwise an empty condition
-     */
-    public IsInCaseInsensitiveWhenPresent map(UnaryOperator<String> mapper) {
+    @Override
+    public <R> IsInCaseInsensitiveWhenPresent<R> map(Function<? super @NonNull T, ? extends @Nullable R> mapper) {
         return mapSupport(mapper, IsInCaseInsensitiveWhenPresent::new, IsInCaseInsensitiveWhenPresent::empty);
     }
 
-    public static IsInCaseInsensitiveWhenPresent of(String... values) {
+    @SafeVarargs
+    public static <T> IsInCaseInsensitiveWhenPresent<T> of(@Nullable T... values) {
         return of(Arrays.asList(values));
     }
 
-    public static IsInCaseInsensitiveWhenPresent of(Collection<String> values) {
-        return new IsInCaseInsensitiveWhenPresent(values).map(StringUtilities::safelyUpperCase);
+    public static <T> IsInCaseInsensitiveWhenPresent<T> 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 fc2994ea7..abacd2690 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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -21,11 +21,13 @@
 import java.util.Objects;
 import java.util.function.Function;
 import java.util.function.Predicate;
-import java.util.stream.Collectors;
 
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
 import org.mybatis.dynamic.sql.AbstractListValueCondition;
 
-public class IsInWhenPresent<T> extends AbstractListValueCondition<T> {
+public class IsInWhenPresent<T> extends AbstractListValueCondition<T>
+        implements AbstractListValueCondition.Filterable<T>, AbstractListValueCondition.Mappable<T> {
     private static final IsInWhenPresent<?> EMPTY = new IsInWhenPresent<>(Collections.emptyList());
 
     public static <T> IsInWhenPresent<T> empty() {
@@ -35,7 +37,7 @@ public static <T> IsInWhenPresent<T> empty() {
     }
 
     protected IsInWhenPresent(Collection<T> values) {
-        super(values.stream().filter(Objects::nonNull).collect(Collectors.toList()));
+        super(values.stream().filter(Objects::nonNull).toList());
     }
 
     @Override
@@ -44,29 +46,25 @@ public String operator() {
     }
 
     @Override
-    public IsInWhenPresent<T> filter(Predicate<? super T> predicate) {
+    public IsInWhenPresent<T> filter(Predicate<? super @NonNull T> predicate) {
         return filterSupport(predicate, IsInWhenPresent::new, this, IsInWhenPresent::empty);
     }
 
-    /**
-     * If not empty, apply the mapping to each value in the list return a new condition with the mapped values.
-     *     Else return an empty condition (this).
-     *
-     * @param mapper a mapping function to apply to the values, if not empty
-     * @param <R> type of the new condition
-     * @return a new condition with mapped values if renderable, otherwise an empty condition
-     */
-    public <R> IsInWhenPresent<R> map(Function<? super T, ? extends R> mapper) {
-        Function<Collection<R>, IsInWhenPresent<R>> constructor = IsInWhenPresent::new;
-        return mapSupport(mapper, constructor, IsInWhenPresent::empty);
+    @Override
+    public <R> IsInWhenPresent<R> map(Function<? super @NonNull T, ? extends @Nullable R> mapper) {
+        return mapSupport(mapper, IsInWhenPresent::of, IsInWhenPresent::empty);
     }
 
     @SafeVarargs
-    public static <T> IsInWhenPresent<T> of(T... values) {
+    public static <T> IsInWhenPresent<T> of(@Nullable T... values) {
         return of(Arrays.asList(values));
     }
 
-    public static <T> IsInWhenPresent<T> of(Collection<T> values) {
-        return new IsInWhenPresent<>(values);
+    public static <T> IsInWhenPresent<T> 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 51012cfa2..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,7 +15,6 @@
  */
 package org.mybatis.dynamic.sql.where.condition;
 
-import org.jetbrains.annotations.NotNull;
 import org.mybatis.dynamic.sql.AbstractSubselectCondition;
 import org.mybatis.dynamic.sql.select.SelectModel;
 import org.mybatis.dynamic.sql.util.Buildable;
@@ -26,7 +25,6 @@ protected IsInWithSubselect(Buildable<SelectModel> selectModelBuilder) {
         super(selectModelBuilder);
     }
 
-    @NotNull
     public static <T> IsInWithSubselect<T> of(Buildable<SelectModel> selectModelBuilder) {
         return new IsInWithSubselect<>(selectModelBuilder);
     }
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 516331e0b..ffe66bd97 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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,13 +15,22 @@
  */
 package org.mybatis.dynamic.sql.where.condition;
 
+import java.util.NoSuchElementException;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
+import org.jspecify.annotations.NonNull;
 import org.mybatis.dynamic.sql.AbstractSingleValueCondition;
 
-public class IsLessThan<T> extends AbstractSingleValueCondition<T> {
-    private static final IsLessThan<?> EMPTY = new IsLessThan<Object>(null) {
+public class IsLessThan<T> extends AbstractSingleValueCondition<T>
+        implements AbstractSingleValueCondition.Filterable<T>, AbstractSingleValueCondition.Mappable<T> {
+
+    private static final IsLessThan<?> EMPTY = new IsLessThan<Object>(-1) {
+        @Override
+        public Object value() {
+            throw new NoSuchElementException("No value present"); //$NON-NLS-1$
+        }
+
         @Override
         public boolean isEmpty() {
             return true;
@@ -48,20 +57,12 @@ public static <T> IsLessThan<T> of(T value) {
     }
 
     @Override
-    public IsLessThan<T> filter(Predicate<? super T> predicate) {
+    public IsLessThan<T> filter(Predicate<? super @NonNull T> predicate) {
         return filterSupport(predicate, IsLessThan::empty, this);
     }
 
-    /**
-     * 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 <R> 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.
-     */
-    public <R> IsLessThan<R> map(Function<? super T, ? extends R> mapper) {
+    @Override
+    public <R> IsLessThan<R> map(Function<? super @NonNull T, ? extends @NonNull R> 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 8d1c200dd..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 83c45cca2..a73707e3e 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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,13 +15,21 @@
  */
 package org.mybatis.dynamic.sql.where.condition;
 
+import java.util.NoSuchElementException;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
+import org.jspecify.annotations.NonNull;
 import org.mybatis.dynamic.sql.AbstractSingleValueCondition;
 
-public class IsLessThanOrEqualTo<T> extends AbstractSingleValueCondition<T> {
-    private static final IsLessThanOrEqualTo<?> EMPTY = new IsLessThanOrEqualTo<Object>(null) {
+public class IsLessThanOrEqualTo<T> extends AbstractSingleValueCondition<T>
+        implements AbstractSingleValueCondition.Filterable<T>, AbstractSingleValueCondition.Mappable<T> {
+    private static final IsLessThanOrEqualTo<?> EMPTY = new IsLessThanOrEqualTo<Object>(-1) {
+        @Override
+        public Object value() {
+            throw new NoSuchElementException("No value present"); //$NON-NLS-1$
+        }
+
         @Override
         public boolean isEmpty() {
             return true;
@@ -48,20 +56,12 @@ public static <T> IsLessThanOrEqualTo<T> of(T value) {
     }
 
     @Override
-    public IsLessThanOrEqualTo<T> filter(Predicate<? super T> predicate) {
+    public IsLessThanOrEqualTo<T> filter(Predicate<? super @NonNull T> predicate) {
         return filterSupport(predicate, IsLessThanOrEqualTo::empty, this);
     }
 
-    /**
-     * 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 <R> 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.
-     */
-    public <R> IsLessThanOrEqualTo<R> map(Function<? super T, ? extends R> mapper) {
+    @Override
+    public <R> IsLessThanOrEqualTo<R> map(Function<? super @NonNull T, ? extends @NonNull R> 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 6c3bd8b65..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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
new file mode 100644
index 000000000..d944b7a45
--- /dev/null
+++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanOrEqualToWhenPresent.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.where.condition;
+
+import java.util.NoSuchElementException;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+import org.mybatis.dynamic.sql.AbstractSingleValueCondition;
+
+public class IsLessThanOrEqualToWhenPresent<T> extends AbstractSingleValueCondition<T>
+        implements AbstractSingleValueCondition.Filterable<T>, AbstractSingleValueCondition.Mappable<T> {
+    private static final IsLessThanOrEqualToWhenPresent<?> EMPTY = new IsLessThanOrEqualToWhenPresent<Object>(-1) {
+        @Override
+        public Object value() {
+            throw new NoSuchElementException("No value present"); //$NON-NLS-1$
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return true;
+        }
+    };
+
+    public static <T> IsLessThanOrEqualToWhenPresent<T> empty() {
+        @SuppressWarnings("unchecked")
+        IsLessThanOrEqualToWhenPresent<T> t = (IsLessThanOrEqualToWhenPresent<T>) EMPTY;
+        return t;
+    }
+
+    protected IsLessThanOrEqualToWhenPresent(T value) {
+        super(value);
+    }
+
+    @Override
+    public String operator() {
+        return "<="; //$NON-NLS-1$
+    }
+
+    public static <T> IsLessThanOrEqualToWhenPresent<T> of(@Nullable T value) {
+        if (value == null) {
+            return empty();
+        } else {
+            return new IsLessThanOrEqualToWhenPresent<>(value);
+        }
+    }
+
+    @Override
+    public IsLessThanOrEqualToWhenPresent<T> filter(Predicate<? super @NonNull T> predicate) {
+        return filterSupport(predicate, IsLessThanOrEqualToWhenPresent::empty, this);
+    }
+
+    @Override
+    public <R> IsLessThanOrEqualToWhenPresent<R> map(Function<? super @NonNull T, ? extends @Nullable R> 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 07d2b67d7..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,7 +15,6 @@
  */
 package org.mybatis.dynamic.sql.where.condition;
 
-import org.jetbrains.annotations.NotNull;
 import org.mybatis.dynamic.sql.AbstractSubselectCondition;
 import org.mybatis.dynamic.sql.select.SelectModel;
 import org.mybatis.dynamic.sql.util.Buildable;
@@ -26,7 +25,6 @@ protected IsLessThanOrEqualToWithSubselect(Buildable<SelectModel> selectModelBui
         super(selectModelBuilder);
     }
 
-    @NotNull
     public static <T> IsLessThanOrEqualToWithSubselect<T> of(Buildable<SelectModel> selectModelBuilder) {
         return new IsLessThanOrEqualToWithSubselect<>(selectModelBuilder);
     }
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
new file mode 100644
index 000000000..830bf788c
--- /dev/null
+++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanWhenPresent.java
@@ -0,0 +1,73 @@
+/*
+ *    Copyright 2016-2025 the original author or authors.
+ *
+ *    Licensed under the Apache License, Version 2.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.NoSuchElementException;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+import org.mybatis.dynamic.sql.AbstractSingleValueCondition;
+
+public class IsLessThanWhenPresent<T> extends AbstractSingleValueCondition<T>
+        implements AbstractSingleValueCondition.Filterable<T>, AbstractSingleValueCondition.Mappable<T> {
+
+    private static final IsLessThanWhenPresent<?> EMPTY = new IsLessThanWhenPresent<Object>(-1) {
+        @Override
+        public Object value() {
+            throw new NoSuchElementException("No value present"); //$NON-NLS-1$
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return true;
+        }
+    };
+
+    public static <T> IsLessThanWhenPresent<T> empty() {
+        @SuppressWarnings("unchecked")
+        IsLessThanWhenPresent<T> t = (IsLessThanWhenPresent<T>) EMPTY;
+        return t;
+    }
+
+    protected IsLessThanWhenPresent(T value) {
+        super(value);
+    }
+
+    @Override
+    public String operator() {
+        return "<"; //$NON-NLS-1$
+    }
+
+    public static <T> IsLessThanWhenPresent<T> of(@Nullable T value) {
+        if (value == null) {
+            return empty();
+        } else {
+            return new IsLessThanWhenPresent<>(value);
+        }
+    }
+
+    @Override
+    public IsLessThanWhenPresent<T> filter(Predicate<? super @NonNull T> predicate) {
+        return filterSupport(predicate, IsLessThanWhenPresent::empty, this);
+    }
+
+    @Override
+    public <R> IsLessThanWhenPresent<R> map(Function<? super @NonNull T, ? extends @Nullable R> 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 21a96e6d1..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,7 +15,6 @@
  */
 package org.mybatis.dynamic.sql.where.condition;
 
-import org.jetbrains.annotations.NotNull;
 import org.mybatis.dynamic.sql.AbstractSubselectCondition;
 import org.mybatis.dynamic.sql.select.SelectModel;
 import org.mybatis.dynamic.sql.util.Buildable;
@@ -26,7 +25,6 @@ protected IsLessThanWithSubselect(Buildable<SelectModel> selectModelBuilder) {
         super(selectModelBuilder);
     }
 
-    @NotNull
     public static <T> IsLessThanWithSubselect<T> of(Buildable<SelectModel> selectModelBuilder) {
         return new IsLessThanWithSubselect<>(selectModelBuilder);
     }
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 864ddb432..f2d2a419a 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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,13 +15,22 @@
  */
 package org.mybatis.dynamic.sql.where.condition;
 
+import java.util.NoSuchElementException;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
+import org.jspecify.annotations.NonNull;
 import org.mybatis.dynamic.sql.AbstractSingleValueCondition;
 
-public class IsLike<T> extends AbstractSingleValueCondition<T> {
-    private static final IsLike<?> EMPTY = new IsLike<Object>(null) {
+public class IsLike<T> extends AbstractSingleValueCondition<T>
+        implements AbstractSingleValueCondition.Filterable<T>, AbstractSingleValueCondition.Mappable<T> {
+
+    private static final IsLike<?> EMPTY = new IsLike<Object>(-1) {
+        @Override
+        public Object value() {
+            throw new NoSuchElementException("No value present"); //$NON-NLS-1$
+        }
+
         @Override
         public boolean isEmpty() {
             return true;
@@ -48,20 +57,12 @@ public static <T> IsLike<T> of(T value) {
     }
 
     @Override
-    public IsLike<T> filter(Predicate<? super T> predicate) {
+    public IsLike<T> filter(Predicate<? super @NonNull T> predicate) {
         return filterSupport(predicate, IsLike::empty, this);
     }
 
-    /**
-     * 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 <R> 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.
-     */
-    public <R> IsLike<R> map(Function<? super T, ? extends R> mapper) {
+    @Override
+    public <R> IsLike<R> map(Function<? super @NonNull T, ? extends @NonNull R> 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 3b13d1748..43525e287 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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,27 +15,37 @@
  */
 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.UnaryOperator;
 
+import org.jspecify.annotations.NonNull;
 import org.mybatis.dynamic.sql.AbstractSingleValueCondition;
 import org.mybatis.dynamic.sql.util.StringUtilities;
 
-public class IsLikeCaseInsensitive extends AbstractSingleValueCondition<String>
-        implements CaseInsensitiveVisitableCondition {
-    private static final IsLikeCaseInsensitive EMPTY = new IsLikeCaseInsensitive(null) {
+public class IsLikeCaseInsensitive<T> extends AbstractSingleValueCondition<T>
+        implements CaseInsensitiveRenderableCondition<T>, AbstractSingleValueCondition.Filterable<T>,
+        AbstractSingleValueCondition.Mappable<T> {
+    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() {
-        return EMPTY;
+    public static <T> IsLikeCaseInsensitive<T> empty() {
+        @SuppressWarnings("unchecked")
+        IsLikeCaseInsensitive<T> t = (IsLikeCaseInsensitive<T>) EMPTY;
+        return t;
     }
 
-    protected IsLikeCaseInsensitive(String value) {
-        super(value);
+    protected IsLikeCaseInsensitive(T value) {
+        super(StringUtilities.upperCaseIfPossible(value));
     }
 
     @Override
@@ -44,23 +54,16 @@ public String operator() {
     }
 
     @Override
-    public IsLikeCaseInsensitive filter(Predicate<? super String> predicate) {
+    public IsLikeCaseInsensitive<T> filter(Predicate<? super @NonNull T> predicate) {
         return filterSupport(predicate, IsLikeCaseInsensitive::empty, this);
     }
 
-    /**
-     * 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
-     * @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.
-     */
-    public IsLikeCaseInsensitive map(UnaryOperator<String> mapper) {
+    @Override
+    public <R> IsLikeCaseInsensitive<R> map(Function<? super @NonNull T, ? extends @NonNull R> mapper) {
         return mapSupport(mapper, IsLikeCaseInsensitive::new, IsLikeCaseInsensitive::empty);
     }
 
-    public static IsLikeCaseInsensitive of(String value) {
-        return new IsLikeCaseInsensitive(value).map(StringUtilities::safelyUpperCase);
+    public static <T> IsLikeCaseInsensitive<T> of(T value) {
+        return new IsLikeCaseInsensitive<>(value);
     }
 }
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
new file mode 100644
index 000000000..60307625f
--- /dev/null
+++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLikeCaseInsensitiveWhenPresent.java
@@ -0,0 +1,75 @@
+/*
+ *    Copyright 2016-2025 the original author or authors.
+ *
+ *    Licensed under the Apache License, Version 2.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.NoSuchElementException;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+import org.mybatis.dynamic.sql.AbstractSingleValueCondition;
+import org.mybatis.dynamic.sql.util.StringUtilities;
+
+public class IsLikeCaseInsensitiveWhenPresent<T> extends AbstractSingleValueCondition<T>
+        implements CaseInsensitiveRenderableCondition<T>, AbstractSingleValueCondition.Filterable<T>,
+        AbstractSingleValueCondition.Mappable<T> {
+    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 <T> IsLikeCaseInsensitiveWhenPresent<T> empty() {
+        @SuppressWarnings("unchecked")
+        IsLikeCaseInsensitiveWhenPresent<T> t = (IsLikeCaseInsensitiveWhenPresent<T>) EMPTY;
+        return t;
+    }
+
+    protected IsLikeCaseInsensitiveWhenPresent(T value) {
+        super(StringUtilities.upperCaseIfPossible(value));
+    }
+
+    @Override
+    public String operator() {
+        return "like"; //$NON-NLS-1$
+    }
+
+    @Override
+    public IsLikeCaseInsensitiveWhenPresent<T> filter(Predicate<? super @NonNull T> predicate) {
+        return filterSupport(predicate, IsLikeCaseInsensitiveWhenPresent::empty, this);
+    }
+
+    @Override
+    public <R> IsLikeCaseInsensitiveWhenPresent<R> map(Function<? super @NonNull T, ? extends @Nullable R> mapper) {
+        return mapSupport(mapper, IsLikeCaseInsensitiveWhenPresent::of, IsLikeCaseInsensitiveWhenPresent::empty);
+    }
+
+    public static <T> IsLikeCaseInsensitiveWhenPresent<T> of(@Nullable T value) {
+        if (value == null) {
+            return empty();
+        } else {
+            return new IsLikeCaseInsensitiveWhenPresent<>(value);
+        }
+    }
+}
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
new file mode 100644
index 000000000..fc20722a2
--- /dev/null
+++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLikeWhenPresent.java
@@ -0,0 +1,73 @@
+/*
+ *    Copyright 2016-2025 the original author or authors.
+ *
+ *    Licensed under the Apache License, Version 2.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.NoSuchElementException;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+import org.mybatis.dynamic.sql.AbstractSingleValueCondition;
+
+public class IsLikeWhenPresent<T> extends AbstractSingleValueCondition<T>
+        implements AbstractSingleValueCondition.Filterable<T>, AbstractSingleValueCondition.Mappable<T> {
+
+    private static final IsLikeWhenPresent<?> EMPTY = new IsLikeWhenPresent<Object>(-1) {
+        @Override
+        public Object value() {
+            throw new NoSuchElementException("No value present"); //$NON-NLS-1$
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return true;
+        }
+    };
+
+    public static <T> IsLikeWhenPresent<T> empty() {
+        @SuppressWarnings("unchecked")
+        IsLikeWhenPresent<T> t = (IsLikeWhenPresent<T>) EMPTY;
+        return t;
+    }
+
+    protected IsLikeWhenPresent(T value) {
+        super(value);
+    }
+
+    @Override
+    public String operator() {
+        return "like"; //$NON-NLS-1$
+    }
+
+    public static <T> IsLikeWhenPresent<T> of(@Nullable T value) {
+        if (value == null) {
+            return empty();
+        } else {
+            return new IsLikeWhenPresent<>(value);
+        }
+    }
+
+    @Override
+    public IsLikeWhenPresent<T> filter(Predicate<? super @NonNull T> predicate) {
+        return filterSupport(predicate, IsLikeWhenPresent::empty, this);
+    }
+
+    @Override
+    public <R> IsLikeWhenPresent<R> map(Function<? super @NonNull T, ? extends @Nullable R> 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 f380449fd..b3d0d59ff 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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,15 +15,27 @@
  */
 package org.mybatis.dynamic.sql.where.condition;
 
-import java.util.Objects;
+import java.util.NoSuchElementException;
 import java.util.function.BiPredicate;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
+import org.jspecify.annotations.NonNull;
 import org.mybatis.dynamic.sql.AbstractTwoValueCondition;
 
-public class IsNotBetween<T> extends AbstractTwoValueCondition<T> {
-    private static final IsNotBetween<?> EMPTY = new IsNotBetween<Object>(null, null) {
+public class IsNotBetween<T> extends AbstractTwoValueCondition<T>
+        implements AbstractTwoValueCondition.Filterable<T>, AbstractTwoValueCondition.Mappable<T> {
+    private static final IsNotBetween<?> EMPTY = new IsNotBetween<Object>(-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;
@@ -51,40 +63,23 @@ public String operator2() {
     }
 
     @Override
-    public IsNotBetween<T> filter(BiPredicate<? super T, ? super T> predicate) {
+    public IsNotBetween<T> filter(BiPredicate<? super @NonNull T, ? super @NonNull T> predicate) {
         return filterSupport(predicate, IsNotBetween::empty, this);
     }
 
     @Override
-    public IsNotBetween<T> filter(Predicate<? super T> predicate) {
+    public IsNotBetween<T> filter(Predicate<? super @NonNull T> predicate) {
         return filterSupport(predicate, IsNotBetween::empty, this);
     }
 
-    /**
-     * 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 <R> 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.
-     */
-    public <R> IsNotBetween<R> map(Function<? super T, ? extends R> mapper1,
-            Function<? super T, ? extends R> mapper2) {
+    @Override
+    public <R> IsNotBetween<R> map(Function<? super @NonNull T, ? extends @NonNull R> mapper1,
+            Function<? super @NonNull T, ? extends @NonNull R> mapper2) {
         return mapSupport(mapper1, mapper2, IsNotBetween::new, IsNotBetween::empty);
     }
 
-    /**
-     * 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 <R> 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.
-     */
-    public <R> IsNotBetween<R> map(Function<? super T, ? extends R> mapper) {
+    @Override
+    public <R> IsNotBetween<R> map(Function<? super @NonNull T, ? extends @NonNull R> mapper) {
         return map(mapper, mapper);
     }
 
@@ -92,10 +87,6 @@ public static <T> Builder<T> isNotBetween(T value1) {
         return new Builder<>(value1);
     }
 
-    public static <T> WhenPresentBuilder<T> isNotBetweenWhenPresent(T value1) {
-        return new WhenPresentBuilder<>(value1);
-    }
-
     public static class Builder<T> extends AndGatherer<T, IsNotBetween<T>> {
 
         private Builder(T value1) {
@@ -103,20 +94,8 @@ private Builder(T value1) {
         }
 
         @Override
-        protected IsNotBetween<T> build() {
+        protected IsNotBetween<T> build(T value2) {
             return new IsNotBetween<>(value1, value2);
         }
     }
-
-    public static class WhenPresentBuilder<T> extends AndGatherer<T, IsNotBetween<T>> {
-
-        private WhenPresentBuilder(T value1) {
-            super(value1);
-        }
-
-        @Override
-        protected IsNotBetween<T> build() {
-            return new IsNotBetween<>(value1, value2).filter(Objects::nonNull);
-        }
-    }
 }
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
new file mode 100644
index 000000000..3c9c8fc9f
--- /dev/null
+++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotBetweenWhenPresent.java
@@ -0,0 +1,110 @@
+/*
+ *    Copyright 2016-2025 the original author or authors.
+ *
+ *    Licensed under the Apache License, Version 2.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.NoSuchElementException;
+import java.util.function.BiPredicate;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+import org.mybatis.dynamic.sql.AbstractTwoValueCondition;
+
+public class IsNotBetweenWhenPresent<T> extends AbstractTwoValueCondition<T>
+        implements AbstractTwoValueCondition.Filterable<T>, AbstractTwoValueCondition.Mappable<T> {
+    private static final IsNotBetweenWhenPresent<?> EMPTY = new IsNotBetweenWhenPresent<Object>(-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;
+        }
+    };
+
+    public static <T> IsNotBetweenWhenPresent<T> empty() {
+        @SuppressWarnings("unchecked")
+        IsNotBetweenWhenPresent<T> t = (IsNotBetweenWhenPresent<T>) 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<T> filter(BiPredicate<? super @NonNull T, ? super @NonNull T> predicate) {
+        return filterSupport(predicate, IsNotBetweenWhenPresent::empty, this);
+    }
+
+    @Override
+    public IsNotBetweenWhenPresent<T> filter(Predicate<? super @NonNull T> predicate) {
+        return filterSupport(predicate, IsNotBetweenWhenPresent::empty, this);
+    }
+
+    @Override
+    public <R> IsNotBetweenWhenPresent<R> map(Function<? super @NonNull T, ? extends @Nullable R> mapper1,
+                                              Function<? super @NonNull T, ? extends @Nullable R> mapper2) {
+        return mapSupport(mapper1, mapper2, IsNotBetweenWhenPresent::of, IsNotBetweenWhenPresent::empty);
+    }
+
+    @Override
+    public <R> IsNotBetweenWhenPresent<R> map(Function<? super @NonNull T, ? extends @Nullable R> mapper) {
+        return map(mapper, mapper);
+    }
+
+    public static <T> IsNotBetweenWhenPresent<T> of(@Nullable T value1, @Nullable T value2) {
+        if (value1 == null || value2 == null) {
+            return empty();
+        } else {
+            return new IsNotBetweenWhenPresent<>(value1, value2);
+        }
+    }
+
+    public static <T> Builder<T> isNotBetweenWhenPresent(@Nullable T value1) {
+        return new Builder<>(value1);
+    }
+
+    public static class Builder<T> extends AndWhenPresentGatherer<T, IsNotBetweenWhenPresent<T>> {
+
+        private Builder(@Nullable T value1) {
+            super(value1);
+        }
+
+        @Override
+        protected IsNotBetweenWhenPresent<T> 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 f35516e56..fc0292070 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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,13 +15,21 @@
  */
 package org.mybatis.dynamic.sql.where.condition;
 
+import java.util.NoSuchElementException;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
+import org.jspecify.annotations.NonNull;
 import org.mybatis.dynamic.sql.AbstractSingleValueCondition;
 
-public class IsNotEqualTo<T> extends AbstractSingleValueCondition<T> {
-    private static final IsNotEqualTo<?> EMPTY = new IsNotEqualTo<Object>(null) {
+public class IsNotEqualTo<T> extends AbstractSingleValueCondition<T>
+        implements AbstractSingleValueCondition.Filterable<T>, AbstractSingleValueCondition.Mappable<T> {
+    private static final IsNotEqualTo<?> EMPTY = new IsNotEqualTo<Object>(-1) {
+        @Override
+        public Object value() {
+            throw new NoSuchElementException("No value present"); //$NON-NLS-1$
+        }
+
         @Override
         public boolean isEmpty() {
             return true;
@@ -48,20 +56,12 @@ public static <T> IsNotEqualTo<T> of(T value) {
     }
 
     @Override
-    public IsNotEqualTo<T> filter(Predicate<? super T> predicate) {
+    public IsNotEqualTo<T> filter(Predicate<? super @NonNull T> predicate) {
         return filterSupport(predicate, IsNotEqualTo::empty, this);
     }
 
-    /**
-     * 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 <R> 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.
-     */
-    public <R> IsNotEqualTo<R> map(Function<? super T, ? extends R> mapper) {
+    @Override
+    public <R> IsNotEqualTo<R> map(Function<? super @NonNull T, ? extends @NonNull R> 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 7dc52aa26..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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
new file mode 100644
index 000000000..1fff2d9ba
--- /dev/null
+++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotEqualToWhenPresent.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.where.condition;
+
+import java.util.NoSuchElementException;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+import org.mybatis.dynamic.sql.AbstractSingleValueCondition;
+
+public class IsNotEqualToWhenPresent<T> extends AbstractSingleValueCondition<T>
+        implements AbstractSingleValueCondition.Filterable<T>, AbstractSingleValueCondition.Mappable<T> {
+    private static final IsNotEqualToWhenPresent<?> EMPTY = new IsNotEqualToWhenPresent<Object>(-1) {
+        @Override
+        public Object value() {
+            throw new NoSuchElementException("No value present"); //$NON-NLS-1$
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return true;
+        }
+    };
+
+    public static <T> IsNotEqualToWhenPresent<T> empty() {
+        @SuppressWarnings("unchecked")
+        IsNotEqualToWhenPresent<T> t = (IsNotEqualToWhenPresent<T>) EMPTY;
+        return t;
+    }
+
+    protected IsNotEqualToWhenPresent(T value) {
+        super(value);
+    }
+
+    @Override
+    public String operator() {
+        return "<>"; //$NON-NLS-1$
+    }
+
+    public static <T> IsNotEqualToWhenPresent<T> of(@Nullable T value) {
+        if (value == null) {
+            return empty();
+        } else {
+            return new IsNotEqualToWhenPresent<>(value);
+        }
+    }
+
+    @Override
+    public IsNotEqualToWhenPresent<T> filter(Predicate<? super @NonNull T> predicate) {
+        return filterSupport(predicate, IsNotEqualToWhenPresent::empty, this);
+    }
+
+    @Override
+    public <R> IsNotEqualToWhenPresent<R> map(Function<? super @NonNull T, ? extends @Nullable R> 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 a9095e0c0..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,7 +15,6 @@
  */
 package org.mybatis.dynamic.sql.where.condition;
 
-import org.jetbrains.annotations.NotNull;
 import org.mybatis.dynamic.sql.AbstractSubselectCondition;
 import org.mybatis.dynamic.sql.select.SelectModel;
 import org.mybatis.dynamic.sql.util.Buildable;
@@ -26,7 +25,6 @@ protected IsNotEqualToWithSubselect(Buildable<SelectModel> selectModelBuilder) {
         super(selectModelBuilder);
     }
 
-    @NotNull
     public static <T> IsNotEqualToWithSubselect<T> of(Buildable<SelectModel> selectModelBuilder) {
         return new IsNotEqualToWithSubselect<>(selectModelBuilder);
     }
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 dc7358b2a..af6c248f4 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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -21,10 +21,13 @@
 import java.util.function.Function;
 import java.util.function.Predicate;
 
+import org.jspecify.annotations.NonNull;
 import org.mybatis.dynamic.sql.AbstractListValueCondition;
 import org.mybatis.dynamic.sql.render.RenderingContext;
+import org.mybatis.dynamic.sql.util.Validator;
 
-public class IsNotIn<T> extends AbstractListValueCondition<T> {
+public class IsNotIn<T> extends AbstractListValueCondition<T>
+        implements AbstractListValueCondition.Filterable<T>, AbstractListValueCondition.Mappable<T> {
     private static final IsNotIn<?> EMPTY = new IsNotIn<>(Collections.emptyList());
 
     public static <T> IsNotIn<T> empty() {
@@ -39,6 +42,7 @@ protected IsNotIn(Collection<T> values) {
 
     @Override
     public boolean shouldRender(RenderingContext renderingContext) {
+        Validator.assertNotEmpty(values, "ERROR.44", "IsNotIn"); //$NON-NLS-1$ //$NON-NLS-2$
         return true;
     }
 
@@ -48,21 +52,13 @@ public String operator() {
     }
 
     @Override
-    public IsNotIn<T> filter(Predicate<? super T> predicate) {
+    public IsNotIn<T> filter(Predicate<? super @NonNull T> predicate) {
         return filterSupport(predicate, IsNotIn::new, this, IsNotIn::empty);
     }
 
-    /**
-     * If not empty, apply the mapping to each value in the list return a new condition with the mapped values.
-     *     Else return an empty condition (this).
-     *
-     * @param mapper a mapping function to apply to the values, if not empty
-     * @param <R> type of the new condition
-     * @return a new condition with mapped values if renderable, otherwise an empty condition
-     */
-    public <R> IsNotIn<R> map(Function<? super T, ? extends R> mapper) {
-        Function<Collection<R>, IsNotIn<R>> constructor = IsNotIn::new;
-        return mapSupport(mapper, constructor, IsNotIn::empty);
+    @Override
+    public <R> IsNotIn<R> map(Function<? super @NonNull T, ? extends @NonNull R> mapper) {
+        return mapSupport(mapper, IsNotIn::new, IsNotIn::empty);
     }
 
     @SafeVarargs
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 fa43af0b6..98a9ca9db 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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,27 +18,33 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.function.Function;
 import java.util.function.Predicate;
-import java.util.function.UnaryOperator;
 
+import org.jspecify.annotations.NonNull;
 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<String>
-        implements CaseInsensitiveVisitableCondition {
-    private static final IsNotInCaseInsensitive EMPTY = new IsNotInCaseInsensitive(Collections.emptyList());
+public class IsNotInCaseInsensitive<T> extends AbstractListValueCondition<T>
+        implements CaseInsensitiveRenderableCondition<T>, AbstractListValueCondition.Filterable<T>,
+        AbstractListValueCondition.Mappable<T> {
+    private static final IsNotInCaseInsensitive<?> EMPTY = new IsNotInCaseInsensitive<>(Collections.emptyList());
 
-    public static IsNotInCaseInsensitive empty() {
-        return EMPTY;
+    public static <T> IsNotInCaseInsensitive<T> empty() {
+        @SuppressWarnings("unchecked")
+        IsNotInCaseInsensitive<T> t = (IsNotInCaseInsensitive<T>) EMPTY;
+        return t;
     }
 
-    protected IsNotInCaseInsensitive(Collection<String> values) {
-        super(values);
+    protected IsNotInCaseInsensitive(Collection<T> values) {
+        super(values.stream().map(StringUtilities::upperCaseIfPossible).toList());
     }
 
     @Override
     public boolean shouldRender(RenderingContext renderingContext) {
+        Validator.assertNotEmpty(values, "ERROR.44", "IsNotInCaseInsensitive"); //$NON-NLS-1$ //$NON-NLS-2$
         return true;
     }
 
@@ -48,26 +54,21 @@ public String operator() {
     }
 
     @Override
-    public IsNotInCaseInsensitive filter(Predicate<? super String> predicate) {
+    public IsNotInCaseInsensitive<T> filter(Predicate<? super @NonNull T> predicate) {
         return filterSupport(predicate, IsNotInCaseInsensitive::new, this, IsNotInCaseInsensitive::empty);
     }
 
-    /**
-     * If not empty, apply the mapping to each value in the list return a new condition with the mapped values.
-     *     Else return an empty condition (this).
-     *
-     * @param mapper a mapping function to apply to the values, if not empty
-     * @return a new condition with mapped values if renderable, otherwise an empty condition
-     */
-    public IsNotInCaseInsensitive map(UnaryOperator<String> mapper) {
+    @Override
+    public <R> IsNotInCaseInsensitive<R> map(Function<? super @NonNull T, ? extends @NonNull R> mapper) {
         return mapSupport(mapper, IsNotInCaseInsensitive::new, IsNotInCaseInsensitive::empty);
     }
 
-    public static IsNotInCaseInsensitive of(String... values) {
+    @SafeVarargs
+    public static <T> IsNotInCaseInsensitive<T> of(T... values) {
         return of(Arrays.asList(values));
     }
 
-    public static IsNotInCaseInsensitive of(Collection<String> values) {
-        return new IsNotInCaseInsensitive(values).map(StringUtilities::safelyUpperCase);
+    public static <T> IsNotInCaseInsensitive<T> of(Collection<T> 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 58f0b347e..6852c67fd 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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,23 +19,28 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Objects;
+import java.util.function.Function;
 import java.util.function.Predicate;
-import java.util.function.UnaryOperator;
-import java.util.stream.Collectors;
 
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
 import org.mybatis.dynamic.sql.AbstractListValueCondition;
 import org.mybatis.dynamic.sql.util.StringUtilities;
 
-public class IsNotInCaseInsensitiveWhenPresent extends AbstractListValueCondition<String>
-        implements CaseInsensitiveVisitableCondition {
-    private static final IsNotInCaseInsensitiveWhenPresent EMPTY = new IsNotInCaseInsensitiveWhenPresent(Collections.emptyList());
+public class IsNotInCaseInsensitiveWhenPresent<T> extends AbstractListValueCondition<T>
+        implements CaseInsensitiveRenderableCondition<T>, AbstractListValueCondition.Filterable<T>,
+        AbstractListValueCondition.Mappable<T> {
+    private static final IsNotInCaseInsensitiveWhenPresent<?> EMPTY =
+            new IsNotInCaseInsensitiveWhenPresent<>(Collections.emptyList());
 
-    public static IsNotInCaseInsensitiveWhenPresent empty() {
-        return EMPTY;
+    public static <T> IsNotInCaseInsensitiveWhenPresent<T> empty() {
+        @SuppressWarnings("unchecked")
+        IsNotInCaseInsensitiveWhenPresent<T> t = (IsNotInCaseInsensitiveWhenPresent<T>) EMPTY;
+        return t;
     }
 
-    protected IsNotInCaseInsensitiveWhenPresent(Collection<String> values) {
-        super(values.stream().filter(Objects::nonNull).collect(Collectors.toList()));
+    protected IsNotInCaseInsensitiveWhenPresent(Collection<T> values) {
+        super(values.stream().filter(Objects::nonNull).map(StringUtilities::upperCaseIfPossible).toList());
     }
 
     @Override
@@ -44,26 +49,26 @@ public String operator() {
     }
 
     @Override
-    public IsNotInCaseInsensitiveWhenPresent filter(Predicate<? super String> predicate) {
-        return filterSupport(predicate, IsNotInCaseInsensitiveWhenPresent::new, this, IsNotInCaseInsensitiveWhenPresent::empty);
+    public IsNotInCaseInsensitiveWhenPresent<T> filter(Predicate<? super @NonNull T> predicate) {
+        return filterSupport(predicate, IsNotInCaseInsensitiveWhenPresent::new,
+                this, IsNotInCaseInsensitiveWhenPresent::empty);
     }
 
-    /**
-     * If not empty, apply the mapping to each value in the list return a new condition with the mapped values.
-     *     Else return an empty condition (this).
-     *
-     * @param mapper a mapping function to apply to the values, if not empty
-     * @return a new condition with mapped values if renderable, otherwise an empty condition
-     */
-    public IsNotInCaseInsensitiveWhenPresent map(UnaryOperator<String> mapper) {
+    @Override
+    public <R> IsNotInCaseInsensitiveWhenPresent<R> map(Function<? super @NonNull T, ? extends @Nullable R> mapper) {
         return mapSupport(mapper, IsNotInCaseInsensitiveWhenPresent::new, IsNotInCaseInsensitiveWhenPresent::empty);
     }
 
-    public static IsNotInCaseInsensitiveWhenPresent of(String... values) {
+    @SafeVarargs
+    public static <T> IsNotInCaseInsensitiveWhenPresent<T> of(@Nullable T... values) {
         return of(Arrays.asList(values));
     }
 
-    public static IsNotInCaseInsensitiveWhenPresent of(Collection<String> values) {
-        return new IsNotInCaseInsensitiveWhenPresent(values).map(StringUtilities::safelyUpperCase);
+    public static <T> IsNotInCaseInsensitiveWhenPresent<T> 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 8115413ef..33efb1782 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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -21,11 +21,13 @@
 import java.util.Objects;
 import java.util.function.Function;
 import java.util.function.Predicate;
-import java.util.stream.Collectors;
 
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
 import org.mybatis.dynamic.sql.AbstractListValueCondition;
 
-public class IsNotInWhenPresent<T> extends AbstractListValueCondition<T> {
+public class IsNotInWhenPresent<T> extends AbstractListValueCondition<T>
+        implements AbstractListValueCondition.Filterable<T>, AbstractListValueCondition.Mappable<T> {
     private static final IsNotInWhenPresent<?> EMPTY = new IsNotInWhenPresent<>(Collections.emptyList());
 
     public static <T> IsNotInWhenPresent<T> empty() {
@@ -35,7 +37,7 @@ public static <T> IsNotInWhenPresent<T> empty() {
     }
 
     protected IsNotInWhenPresent(Collection<T> values) {
-        super(values.stream().filter(Objects::nonNull).collect(Collectors.toList()));
+        super(values.stream().filter(Objects::nonNull).toList());
     }
 
     @Override
@@ -44,29 +46,25 @@ public String operator() {
     }
 
     @Override
-    public IsNotInWhenPresent<T> filter(Predicate<? super T> predicate) {
+    public IsNotInWhenPresent<T> filter(Predicate<? super @NonNull T> predicate) {
         return filterSupport(predicate, IsNotInWhenPresent::new, this, IsNotInWhenPresent::empty);
     }
 
-    /**
-     * If not empty, apply the mapping to each value in the list return a new condition with the mapped values.
-     *     Else return an empty condition (this).
-     *
-     * @param mapper a mapping function to apply to the values, if not empty
-     * @param <R> type of the new condition
-     * @return a new condition with mapped values if renderable, otherwise an empty condition
-     */
-    public <R> IsNotInWhenPresent<R> map(Function<? super T, ? extends R> mapper) {
-        Function<Collection<R>, IsNotInWhenPresent<R>> constructor = IsNotInWhenPresent::new;
-        return mapSupport(mapper, constructor, IsNotInWhenPresent::empty);
+    @Override
+    public <R> IsNotInWhenPresent<R> map(Function<? super @NonNull T, ? extends @Nullable R> mapper) {
+        return mapSupport(mapper, IsNotInWhenPresent::new, IsNotInWhenPresent::empty);
     }
 
     @SafeVarargs
-    public static <T> IsNotInWhenPresent<T> of(T... values) {
+    public static <T> IsNotInWhenPresent<T> of(@Nullable T... values) {
         return of(Arrays.asList(values));
     }
 
-    public static <T> IsNotInWhenPresent<T> of(Collection<T> values) {
-        return new IsNotInWhenPresent<>(values);
+    public static <T> IsNotInWhenPresent<T> 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 587994bf8..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,7 +15,6 @@
  */
 package org.mybatis.dynamic.sql.where.condition;
 
-import org.jetbrains.annotations.NotNull;
 import org.mybatis.dynamic.sql.AbstractSubselectCondition;
 import org.mybatis.dynamic.sql.select.SelectModel;
 import org.mybatis.dynamic.sql.util.Buildable;
@@ -26,7 +25,6 @@ protected IsNotInWithSubselect(Buildable<SelectModel> selectModelBuilder) {
         super(selectModelBuilder);
     }
 
-    @NotNull
     public static <T> IsNotInWithSubselect<T> of(Buildable<SelectModel> selectModelBuilder) {
         return new IsNotInWithSubselect<>(selectModelBuilder);
     }
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 79d97ca15..b5b82d675 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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,13 +15,21 @@
  */
 package org.mybatis.dynamic.sql.where.condition;
 
+import java.util.NoSuchElementException;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
+import org.jspecify.annotations.NonNull;
 import org.mybatis.dynamic.sql.AbstractSingleValueCondition;
 
-public class IsNotLike<T> extends AbstractSingleValueCondition<T> {
-    private static final IsNotLike<?> EMPTY = new IsNotLike<Object>(null) {
+public class IsNotLike<T> extends AbstractSingleValueCondition<T>
+        implements AbstractSingleValueCondition.Filterable<T>, AbstractSingleValueCondition.Mappable<T> {
+    private static final IsNotLike<?> EMPTY = new IsNotLike<Object>(-1) {
+        @Override
+        public Object value() {
+            throw new NoSuchElementException("No value present"); //$NON-NLS-1$
+        }
+
         @Override
         public boolean isEmpty() {
             return true;
@@ -48,23 +56,12 @@ public static <T> IsNotLike<T> of(T value) {
     }
 
     @Override
-    public IsNotLike<T> filter(Predicate<? super T> predicate) {
+    public IsNotLike<T> filter(Predicate<? super @NonNull T> predicate) {
         return filterSupport(predicate, IsNotLike::empty, this);
     }
 
-    /**
-     * 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 <R>
-     *            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.
-     */
-    public <R> IsNotLike<R> map(Function<? super T, ? extends R> mapper) {
+    @Override
+    public <R> IsNotLike<R> map(Function<? super @NonNull T, ? extends @NonNull R> 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 eb0450135..1604deb3b 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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,27 +15,37 @@
  */
 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.UnaryOperator;
 
+import org.jspecify.annotations.NonNull;
 import org.mybatis.dynamic.sql.AbstractSingleValueCondition;
 import org.mybatis.dynamic.sql.util.StringUtilities;
 
-public class IsNotLikeCaseInsensitive extends AbstractSingleValueCondition<String>
-        implements CaseInsensitiveVisitableCondition {
-    private static final IsNotLikeCaseInsensitive EMPTY = new IsNotLikeCaseInsensitive(null) {
+public class IsNotLikeCaseInsensitive<T> extends AbstractSingleValueCondition<T>
+        implements CaseInsensitiveRenderableCondition<T>, AbstractSingleValueCondition.Filterable<T>,
+        AbstractSingleValueCondition.Mappable<T> {
+    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() {
-        return EMPTY;
+    public static <T> IsNotLikeCaseInsensitive<T> empty() {
+        @SuppressWarnings("unchecked")
+        IsNotLikeCaseInsensitive<T> t = (IsNotLikeCaseInsensitive<T>) EMPTY;
+        return t;
     }
 
-    protected IsNotLikeCaseInsensitive(String value) {
-        super(value);
+    protected IsNotLikeCaseInsensitive(T value) {
+        super(StringUtilities.upperCaseIfPossible(value));
     }
 
     @Override
@@ -44,25 +54,16 @@ public String operator() {
     }
 
     @Override
-    public IsNotLikeCaseInsensitive filter(Predicate<? super String> predicate) {
+    public IsNotLikeCaseInsensitive<T> filter(Predicate<? super @NonNull T> predicate) {
         return filterSupport(predicate, IsNotLikeCaseInsensitive::empty, this);
     }
 
-    /**
-     * 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
-     *
-     * @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.
-     */
-    public IsNotLikeCaseInsensitive map(UnaryOperator<String> mapper) {
+    @Override
+    public <R> IsNotLikeCaseInsensitive<R> map(Function<? super @NonNull T, ? extends @NonNull R> mapper) {
         return mapSupport(mapper, IsNotLikeCaseInsensitive::new, IsNotLikeCaseInsensitive::empty);
     }
 
-    public static IsNotLikeCaseInsensitive of(String value) {
-        return new IsNotLikeCaseInsensitive(value).map(StringUtilities::safelyUpperCase);
+    public static <T> IsNotLikeCaseInsensitive<T> of(T value) {
+        return new IsNotLikeCaseInsensitive<>(value);
     }
 }
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
new file mode 100644
index 000000000..cc0b04549
--- /dev/null
+++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotLikeCaseInsensitiveWhenPresent.java
@@ -0,0 +1,75 @@
+/*
+ *    Copyright 2016-2025 the original author or authors.
+ *
+ *    Licensed under the Apache License, Version 2.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.NoSuchElementException;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+import org.mybatis.dynamic.sql.AbstractSingleValueCondition;
+import org.mybatis.dynamic.sql.util.StringUtilities;
+
+public class IsNotLikeCaseInsensitiveWhenPresent<T> extends AbstractSingleValueCondition<T>
+        implements CaseInsensitiveRenderableCondition<T>, AbstractSingleValueCondition.Filterable<T>,
+        AbstractSingleValueCondition.Mappable<T> {
+    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 <T> IsNotLikeCaseInsensitiveWhenPresent<T> empty() {
+        @SuppressWarnings("unchecked")
+        IsNotLikeCaseInsensitiveWhenPresent<T> t = (IsNotLikeCaseInsensitiveWhenPresent<T>) EMPTY;
+        return t;
+    }
+
+    protected IsNotLikeCaseInsensitiveWhenPresent(T value) {
+        super(StringUtilities.upperCaseIfPossible(value));
+    }
+
+    @Override
+    public String operator() {
+        return "not like"; //$NON-NLS-1$
+    }
+
+    @Override
+    public IsNotLikeCaseInsensitiveWhenPresent<T> filter(Predicate<? super @NonNull T> predicate) {
+        return filterSupport(predicate, IsNotLikeCaseInsensitiveWhenPresent::empty, this);
+    }
+
+    @Override
+    public <R> IsNotLikeCaseInsensitiveWhenPresent<R> map(Function<? super @NonNull T, ? extends @Nullable R> mapper) {
+        return mapSupport(mapper, IsNotLikeCaseInsensitiveWhenPresent::of, IsNotLikeCaseInsensitiveWhenPresent::empty);
+    }
+
+    public static <T> IsNotLikeCaseInsensitiveWhenPresent<T> of(@Nullable T value) {
+        if (value == null) {
+            return empty();
+        } else {
+            return new IsNotLikeCaseInsensitiveWhenPresent<>(value);
+        }
+    }
+}
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
new file mode 100644
index 000000000..a3571b0ee
--- /dev/null
+++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotLikeWhenPresent.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.where.condition;
+
+import java.util.NoSuchElementException;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+import org.mybatis.dynamic.sql.AbstractSingleValueCondition;
+
+public class IsNotLikeWhenPresent<T> extends AbstractSingleValueCondition<T>
+        implements AbstractSingleValueCondition.Filterable<T>, AbstractSingleValueCondition.Mappable<T> {
+    private static final IsNotLikeWhenPresent<?> EMPTY = new IsNotLikeWhenPresent<Object>(-1) {
+        @Override
+        public Object value() {
+            throw new NoSuchElementException("No value present"); //$NON-NLS-1$
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return true;
+        }
+    };
+
+    public static <T> IsNotLikeWhenPresent<T> empty() {
+        @SuppressWarnings("unchecked")
+        IsNotLikeWhenPresent<T> t = (IsNotLikeWhenPresent<T>) EMPTY;
+        return t;
+    }
+
+    protected IsNotLikeWhenPresent(T value) {
+        super(value);
+    }
+
+    @Override
+    public String operator() {
+        return "not like"; //$NON-NLS-1$
+    }
+
+    public static <T> IsNotLikeWhenPresent<T> of(@Nullable T value) {
+        if (value == null) {
+            return empty();
+        } else {
+            return new IsNotLikeWhenPresent<>(value);
+        }
+    }
+
+    @Override
+    public IsNotLikeWhenPresent<T> filter(Predicate<? super @NonNull T> predicate) {
+        return filterSupport(predicate, IsNotLikeWhenPresent::empty, this);
+    }
+
+    @Override
+    public <R> IsNotLikeWhenPresent<R> map(Function<? super @NonNull T, ? extends @Nullable R> 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 59b4052f8..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,8 +19,8 @@
 
 import org.mybatis.dynamic.sql.AbstractNoValueCondition;
 
-public class IsNotNull<T> extends AbstractNoValueCondition<T> {
-    private static final IsNotNull<?> EMPTY = new IsNotNull<Object>() {
+public class IsNotNull<T> extends AbstractNoValueCondition<T> implements AbstractNoValueCondition.Filterable {
+    private static final IsNotNull<?> EMPTY = new IsNotNull<>() {
         @Override
         public boolean isEmpty() {
             return true;
@@ -42,17 +42,7 @@ public String operator() {
         return "is not null"; //$NON-NLS-1$
     }
 
-    /**
-     * 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 <S>
-     *            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.
-     */
+    @Override
     public <S> IsNotNull<S> filter(BooleanSupplier booleanSupplier) {
         @SuppressWarnings("unchecked")
         IsNotNull<S> self = (IsNotNull<S>) this;
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 61fe53da2..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,8 +19,8 @@
 
 import org.mybatis.dynamic.sql.AbstractNoValueCondition;
 
-public class IsNull<T> extends AbstractNoValueCondition<T> {
-    private static final IsNull<?> EMPTY = new IsNull<Object>() {
+public class IsNull<T> extends AbstractNoValueCondition<T> implements AbstractNoValueCondition.Filterable {
+    private static final IsNull<?> EMPTY = new IsNull<>() {
         @Override
         public boolean isEmpty() {
             return true;
@@ -42,17 +42,7 @@ public String operator() {
         return "is null"; //$NON-NLS-1$
     }
 
-    /**
-     * 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 <S>
-     *            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.
-     */
+    @Override
     public <S> IsNull<S> filter(BooleanSupplier booleanSupplier) {
         @SuppressWarnings("unchecked")
         IsNull<S> self = (IsNull<S>) this;
diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/CaseInsensitiveVisitableCondition.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/package-info.java
similarity index 63%
rename from src/main/java/org/mybatis/dynamic/sql/where/condition/CaseInsensitiveVisitableCondition.java
rename to src/main/java/org/mybatis/dynamic/sql/where/condition/package-info.java
index 18b21a136..3457063de 100644
--- a/src/main/java/org/mybatis/dynamic/sql/where/condition/CaseInsensitiveVisitableCondition.java
+++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/package-info.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -13,14 +13,7 @@
  *    See the License for the specific language governing permissions and
  *    limitations under the License.
  */
+@NullMarked
 package org.mybatis.dynamic.sql.where.condition;
 
-import org.mybatis.dynamic.sql.VisitableCondition;
-
-public interface CaseInsensitiveVisitableCondition extends VisitableCondition<String> {
-
-    @Override
-    default String overrideRenderedLeftColumn(String renderedLeftColumn) {
-        return "upper(" + renderedLeftColumn + ")"; //$NON-NLS-1$ //$NON-NLS-2$
-    }
-}
+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
index 2a78ccbf8..c094bca1b 100644
--- a/src/main/java/org/mybatis/dynamic/sql/where/render/ColumnAndConditionRenderer.java
+++ b/src/main/java/org/mybatis/dynamic/sql/where/render/ColumnAndConditionRenderer.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,18 +15,19 @@
  */
 package org.mybatis.dynamic.sql.where.render;
 
-import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore;
-
 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.VisitableCondition;
+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<T> {
     private final BindableColumn<T> column;
-    private final VisitableCondition<T> condition;
+    private final RenderableCondition<T> condition;
     private final RenderingContext renderingContext;
 
     private ColumnAndConditionRenderer(Builder<T> builder) {
@@ -36,34 +37,23 @@ private ColumnAndConditionRenderer(Builder<T> builder) {
     }
 
     public FragmentAndParameters render() {
-        FragmentAndParameters renderedLeftColumn = column.render(renderingContext);
-
-        DefaultConditionVisitor<T> visitor = DefaultConditionVisitor.withColumn(column)
-                .withRenderingContext(renderingContext)
-                .build();
-
-        FragmentAndParameters renderedCondition = condition.accept(visitor);
-
-        String finalFragment = condition.overrideRenderedLeftColumn(renderedLeftColumn.fragment())
-                + spaceBefore(renderedCondition.fragment());
-
-        return FragmentAndParameters.withFragment(finalFragment)
-                .withParameters(renderedLeftColumn.parameters())
-                .withParameters(renderedCondition.parameters())
-                .build();
+        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<T> {
-        private BindableColumn<T> column;
-        private VisitableCondition<T> condition;
-        private RenderingContext renderingContext;
+        private @Nullable BindableColumn<T> column;
+        private @Nullable RenderableCondition<T> condition;
+        private @Nullable RenderingContext renderingContext;
 
         public Builder<T> withColumn(BindableColumn<T> column) {
             this.column = column;
             return this;
         }
 
-        public Builder<T> withCondition(VisitableCondition<T> condition) {
+        public Builder<T> withCondition(RenderableCondition<T> condition) {
             this.condition = condition;
             return 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 6e3e13254..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -30,7 +30,7 @@
 import org.mybatis.dynamic.sql.SqlCriterion;
 import org.mybatis.dynamic.sql.SqlCriterionVisitor;
 import org.mybatis.dynamic.sql.render.RenderingContext;
-import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
+import org.mybatis.dynamic.sql.select.render.SubQueryRenderer;
 import org.mybatis.dynamic.sql.util.FragmentAndParameters;
 import org.mybatis.dynamic.sql.util.FragmentCollector;
 
@@ -119,25 +119,18 @@ private <T> Optional<FragmentAndParameters> renderColumnAndCondition(ColumnAndCo
 
     private FragmentAndParameters renderExists(ExistsCriterion criterion) {
         ExistsPredicate existsPredicate = criterion.existsPredicate();
-
-        SelectStatementProvider selectStatement = existsPredicate.selectModelBuilder().build().render(renderingContext);
-
-        String fragment = existsPredicate.operator()
-                + " (" //$NON-NLS-1$
-                + selectStatement.getSelectStatement()
-                + ")"; //$NON-NLS-1$
-
-        return FragmentAndParameters
-                .withFragment(fragment)
-                .withParameters(selectStatement.getParameters())
-                .build();
+        return SubQueryRenderer.withSelectModel(existsPredicate.selectModelBuilder().build())
+                .withRenderingContext(renderingContext)
+                .withPrefix(existsPredicate.operator() + " (") //$NON-NLS-1$
+                .withSuffix(")") //$NON-NLS-1$
+                .build()
+                .render();
     }
 
     private List<RenderedCriterion> renderSubCriteria(List<AndOrCriteriaGroup> subCriteria) {
         return subCriteria.stream().map(this::renderAndOrCriteriaGroup)
-                .filter(Optional::isPresent)
-                .map(Optional::get)
-                .collect(Collectors.toList());
+                .flatMap(Optional::stream)
+                .toList();
     }
 
     private Optional<RenderedCriterion> renderAndOrCriteriaGroup(AndOrCriteriaGroup criterion) {
diff --git a/src/main/java/org/mybatis/dynamic/sql/where/render/DefaultConditionVisitor.java b/src/main/java/org/mybatis/dynamic/sql/where/render/DefaultConditionVisitor.java
deleted file mode 100644
index 9adda9f64..000000000
--- a/src/main/java/org/mybatis/dynamic/sql/where/render/DefaultConditionVisitor.java
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- *    Copyright 2016-2024 the original author or authors.
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *       https://www.apache.org/licenses/LICENSE-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 static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore;
-
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-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.RenderedParameterInfo;
-import org.mybatis.dynamic.sql.render.RenderingContext;
-import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
-import org.mybatis.dynamic.sql.util.FragmentAndParameters;
-import org.mybatis.dynamic.sql.util.FragmentCollector;
-
-public class DefaultConditionVisitor<T> implements ConditionVisitor<T, FragmentAndParameters> {
-
-    private final BindableColumn<T> column;
-    private final RenderingContext renderingContext;
-
-    private DefaultConditionVisitor(Builder<T> builder) {
-        column = Objects.requireNonNull(builder.column);
-        renderingContext = Objects.requireNonNull(builder.renderingContext);
-    }
-
-    @Override
-    public FragmentAndParameters visit(AbstractListValueCondition<T> condition) {
-        FragmentCollector fc = condition.values()
-                .map(this::toFragmentAndParameters)
-                .collect(FragmentCollector.collect());
-
-        String joinedFragments =
-                fc.collectFragments(Collectors.joining(",", "(", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
-        String finalFragment = condition.operator()
-                + spaceBefore(joinedFragments);
-
-        return FragmentAndParameters
-                .withFragment(finalFragment)
-                .withParameters(fc.parameters())
-                .build();
-    }
-
-    @Override
-    public FragmentAndParameters visit(AbstractNoValueCondition<T> condition) {
-        return FragmentAndParameters.fromFragment(condition.operator());
-    }
-
-    @Override
-    public FragmentAndParameters visit(AbstractSingleValueCondition<T> condition) {
-        RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo(column);
-        String finalFragment = condition.operator()
-                + spaceBefore(parameterInfo.renderedPlaceHolder());
-
-        return FragmentAndParameters.withFragment(finalFragment)
-                .withParameter(parameterInfo.parameterMapKey(), convertValue(condition.value()))
-                .build();
-    }
-
-    @Override
-    public FragmentAndParameters visit(AbstractTwoValueCondition<T> condition) {
-        RenderedParameterInfo parameterInfo1 = renderingContext.calculateParameterInfo(column);
-        RenderedParameterInfo parameterInfo2 = renderingContext.calculateParameterInfo(column);
-
-        String finalFragment = condition.operator1()
-                + spaceBefore(parameterInfo1.renderedPlaceHolder())
-                + spaceBefore(condition.operator2())
-                + spaceBefore(parameterInfo2.renderedPlaceHolder());
-
-        return FragmentAndParameters.withFragment(finalFragment)
-                .withParameter(parameterInfo1.parameterMapKey(), convertValue(condition.value1()))
-                .withParameter(parameterInfo2.parameterMapKey(), convertValue(condition.value2()))
-                .build();
-    }
-
-    @Override
-    public FragmentAndParameters visit(AbstractSubselectCondition<T> condition) {
-        SelectStatementProvider selectStatement = condition.selectModel().render(renderingContext);
-
-        String finalFragment = condition.operator()
-                + " (" //$NON-NLS-1$
-                + selectStatement.getSelectStatement()
-                + ")"; //$NON-NLS-1$
-
-        return FragmentAndParameters.withFragment(finalFragment)
-                .withParameters(selectStatement.getParameters())
-                .build();
-    }
-
-    @Override
-    public FragmentAndParameters visit(AbstractColumnComparisonCondition<T> condition) {
-        FragmentAndParameters renderedRightColumn = condition.rightColumn().render(renderingContext);
-        String finalFragment = condition.operator()
-                + spaceBefore(renderedRightColumn.fragment());
-        return FragmentAndParameters.withFragment(finalFragment)
-                .withParameters(renderedRightColumn.parameters())
-                .build();
-    }
-
-    private Object convertValue(T value) {
-        return column.convertParameterType(value);
-    }
-
-    private FragmentAndParameters toFragmentAndParameters(T value) {
-        RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo(column);
-        return FragmentAndParameters.withFragment(parameterInfo.renderedPlaceHolder())
-                .withParameter(parameterInfo.parameterMapKey(), convertValue(value))
-                .build();
-    }
-
-    public static <T> Builder<T> withColumn(BindableColumn<T> column) {
-        return new Builder<T>().withColumn(column);
-    }
-
-    public static class Builder<T> {
-        private BindableColumn<T> column;
-        private RenderingContext renderingContext;
-
-        public Builder<T> withColumn(BindableColumn<T> column) {
-            this.column = column;
-            return this;
-        }
-
-        public Builder<T> withRenderingContext(RenderingContext renderingContext) {
-            this.renderingContext = renderingContext;
-            return this;
-        }
-
-        public DefaultConditionVisitor<T> build() {
-            return new DefaultConditionVisitor<>(this);
-        }
-    }
-}
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<String, Object> parameters;
+
+    private DefaultWhereClauseProvider(Builder builder) {
+        whereClause = Objects.requireNonNull(builder.whereClause);
+        parameters = builder.parameters;
+    }
+
+    @Override
+    public Map<String, Object> 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<String, Object> parameters = new HashMap<>();
+
+        public Builder withWhereClause(String whereClause) {
+            this.whereClause = whereClause;
+            return this;
+        }
+
+        public Builder withParameters(Map<String, Object> 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 6d1a2de25..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,10 +19,11 @@
 
 import java.util.Objects;
 
+import org.jspecify.annotations.Nullable;
 import org.mybatis.dynamic.sql.util.FragmentAndParameters;
 
 public class RenderedCriterion {
-    private final String connector;
+    private final @Nullable String connector;
     private final FragmentAndParameters fragmentAndParameters;
 
     private RenderedCriterion(Builder builder) {
@@ -54,8 +55,8 @@ private FragmentAndParameters prependFragment(FragmentAndParameters fragmentAndP
     }
 
     public static class Builder {
-        private String connector;
-        private FragmentAndParameters fragmentAndParameters;
+        private @Nullable String connector;
+        private @Nullable FragmentAndParameters fragmentAndParameters;
 
         public Builder withConnector(String connector) {
             this.connector = connector;
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 37d051e81..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -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 final String whereClause;
-    private final Map<String, Object> parameters;
+public interface WhereClauseProvider {
+    Map<String, Object> getParameters();
 
-    private WhereClauseProvider(Builder builder) {
-        whereClause = Objects.requireNonNull(builder.whereClause);
-        parameters = Objects.requireNonNull(builder.parameters);
-    }
-
-    public Map<String, Object> 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 final Map<String, Object> parameters = new HashMap<>();
-
-        public Builder withWhereClause(String whereClause) {
-            this.whereClause = whereClause;
-            return this;
-        }
-
-        public Builder withParameters(Map<String, Object> 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/WhereRenderer.java b/src/main/java/org/mybatis/dynamic/sql/where/render/WhereRenderer.java
index 244dcfa1b..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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/GroupingCriteriaCollector.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/GroupingCriteriaCollector.kt
index 4fe8d3091..be4ba72af 100644
--- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/GroupingCriteriaCollector.kt
+++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/GroupingCriteriaCollector.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -22,9 +22,9 @@ 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
-import org.mybatis.dynamic.sql.VisitableCondition
 
 typealias GroupingCriteriaReceiver = GroupingCriteriaCollector.() -> Unit
 
@@ -229,21 +229,21 @@ open class GroupingCriteriaCollector : SubCriteriaCollector() {
      *
      * @param condition the condition to be applied to this column, in this scope
      */
-    operator fun <T> BindableColumn<T>.invoke(condition: VisitableCondition<T>) {
+    operator fun <T : Any> BindableColumn<T>.invoke(condition: RenderableCondition<T>) {
         initialCriterion = ColumnAndConditionCriterion.withColumn(this)
             .withCondition(condition)
             .build()
     }
 
-    // infix functions...we may be able to rewrite these as extension functions once Kotlin solves the multiple
-    // receivers problem (https://youtrack.jetbrains.com/issue/KT-42435)
+    // 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 <T> BindableColumn<T>.isEqualTo(value: T & Any) =
+    infix fun <T : Any> BindableColumn<T>.isEqualTo(value: T) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isEqualTo(value))
 
     infix fun BindableColumn<*>.isEqualTo(subQuery: KotlinSubQueryBuilder.() -> Unit) =
@@ -252,10 +252,10 @@ open class GroupingCriteriaCollector : SubCriteriaCollector() {
     infix fun BindableColumn<*>.isEqualTo(column: BasicColumn) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isEqualTo(column))
 
-    infix fun <T> BindableColumn<T>.isEqualToWhenPresent(value: T?) =
+    infix fun <T : Any> BindableColumn<T>.isEqualToWhenPresent(value: T?) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isEqualToWhenPresent(value))
 
-    infix fun <T> BindableColumn<T>.isNotEqualTo(value: T & Any) =
+    infix fun <T : Any> BindableColumn<T>.isNotEqualTo(value: T) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotEqualTo(value))
 
     infix fun BindableColumn<*>.isNotEqualTo(subQuery: KotlinSubQueryBuilder.() -> Unit) =
@@ -264,10 +264,10 @@ open class GroupingCriteriaCollector : SubCriteriaCollector() {
     infix fun BindableColumn<*>.isNotEqualTo(column: BasicColumn) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotEqualTo(column))
 
-    infix fun <T> BindableColumn<T>.isNotEqualToWhenPresent(value: T?) =
+    infix fun <T : Any> BindableColumn<T>.isNotEqualToWhenPresent(value: T?) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotEqualToWhenPresent(value))
 
-    infix fun <T> BindableColumn<T>.isGreaterThan(value: T & Any) =
+    infix fun <T : Any> BindableColumn<T>.isGreaterThan(value: T) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isGreaterThan(value))
 
     infix fun BindableColumn<*>.isGreaterThan(subQuery: KotlinSubQueryBuilder.() -> Unit) =
@@ -276,10 +276,10 @@ open class GroupingCriteriaCollector : SubCriteriaCollector() {
     infix fun BindableColumn<*>.isGreaterThan(column: BasicColumn) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isGreaterThan(column))
 
-    infix fun <T> BindableColumn<T>.isGreaterThanWhenPresent(value: T?) =
+    infix fun <T : Any> BindableColumn<T>.isGreaterThanWhenPresent(value: T?) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isGreaterThanWhenPresent(value))
 
-    infix fun <T> BindableColumn<T>.isGreaterThanOrEqualTo(value: T & Any) =
+    infix fun <T : Any> BindableColumn<T>.isGreaterThanOrEqualTo(value: T) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isGreaterThanOrEqualTo(value))
 
     infix fun BindableColumn<*>.isGreaterThanOrEqualTo(subQuery: KotlinSubQueryBuilder.() -> Unit) =
@@ -288,10 +288,10 @@ open class GroupingCriteriaCollector : SubCriteriaCollector() {
     infix fun BindableColumn<*>.isGreaterThanOrEqualTo(column: BasicColumn) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isGreaterThanOrEqualTo(column))
 
-    infix fun <T> BindableColumn<T>.isGreaterThanOrEqualToWhenPresent(value: T?) =
+    infix fun <T : Any> BindableColumn<T>.isGreaterThanOrEqualToWhenPresent(value: T?) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isGreaterThanOrEqualToWhenPresent(value))
 
-    infix fun <T> BindableColumn<T>.isLessThan(value: T & Any) =
+    infix fun <T : Any> BindableColumn<T>.isLessThan(value: T) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLessThan(value))
 
     infix fun BindableColumn<*>.isLessThan(subQuery: KotlinSubQueryBuilder.() -> Unit) =
@@ -300,10 +300,10 @@ open class GroupingCriteriaCollector : SubCriteriaCollector() {
     infix fun BindableColumn<*>.isLessThan(column: BasicColumn) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLessThan(column))
 
-    infix fun <T> BindableColumn<T>.isLessThanWhenPresent(value: T?) =
+    infix fun <T : Any> BindableColumn<T>.isLessThanWhenPresent(value: T?) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLessThanWhenPresent(value))
 
-    infix fun <T> BindableColumn<T>.isLessThanOrEqualTo(value: T & Any) =
+    infix fun <T : Any> BindableColumn<T>.isLessThanOrEqualTo(value: T) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLessThanOrEqualTo(value))
 
     infix fun BindableColumn<*>.isLessThanOrEqualTo(subQuery: KotlinSubQueryBuilder.() -> Unit) =
@@ -312,82 +312,82 @@ open class GroupingCriteriaCollector : SubCriteriaCollector() {
     infix fun BindableColumn<*>.isLessThanOrEqualTo(column: BasicColumn) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLessThanOrEqualTo(column))
 
-    infix fun <T> BindableColumn<T>.isLessThanOrEqualToWhenPresent(value: T?) =
+    infix fun <T : Any> BindableColumn<T>.isLessThanOrEqualToWhenPresent(value: T?) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLessThanOrEqualToWhenPresent(value))
 
-    fun <T> BindableColumn<T>.isIn(vararg values: T & Any) = isIn(values.asList())
+    fun <T : Any> BindableColumn<T>.isIn(vararg values: T) = isIn(values.asList())
 
     @JvmName("isInArray")
-    infix fun <T> BindableColumn<T>.isIn(values: Array<out T & Any>) =
+    infix fun <T : Any> BindableColumn<T>.isIn(values: Array<out T>) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isIn(values))
 
-    infix fun <T> BindableColumn<T>.isIn(values: Collection<T & Any>) =
+    infix fun <T : Any> BindableColumn<T>.isIn(values: Collection<T>) =
         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 <T> BindableColumn<T>.isInWhenPresent(vararg values: T?) = isInWhenPresent(values.asList())
+    fun <T : Any> BindableColumn<T>.isInWhenPresent(vararg values: T?) = isInWhenPresent(values.asList())
 
     @JvmName("isInArrayWhenPresent")
-    infix fun <T> BindableColumn<T>.isInWhenPresent(values: Array<out T?>?) =
+    infix fun <T : Any> BindableColumn<T>.isInWhenPresent(values: Array<out T?>?) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isInWhenPresent(values))
 
-    infix fun <T> BindableColumn<T>.isInWhenPresent(values: Collection<T?>?) =
+    infix fun <T : Any> BindableColumn<T>.isInWhenPresent(values: Collection<T?>?) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isInWhenPresent(values))
 
-    fun <T> BindableColumn<T>.isNotIn(vararg values: T & Any) = isNotIn(values.asList())
+    fun <T : Any> BindableColumn<T>.isNotIn(vararg values: T) = isNotIn(values.asList())
 
     @JvmName("isNotInArray")
-    infix fun <T> BindableColumn<T>.isNotIn(values: Array<out T & Any>) =
+    infix fun <T : Any> BindableColumn<T>.isNotIn(values: Array<out T>) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotIn(values))
 
-    infix fun <T> BindableColumn<T>.isNotIn(values: Collection<T & Any>) =
+    infix fun <T : Any> BindableColumn<T>.isNotIn(values: Collection<T>) =
         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 <T> BindableColumn<T>.isNotInWhenPresent(vararg values: T?) = isNotInWhenPresent(values.asList())
+    fun <T : Any> BindableColumn<T>.isNotInWhenPresent(vararg values: T?) = isNotInWhenPresent(values.asList())
 
     @JvmName("isNotInArrayWhenPresent")
-    infix fun <T> BindableColumn<T>.isNotInWhenPresent(values: Array<out T?>?) =
+    infix fun <T : Any> BindableColumn<T>.isNotInWhenPresent(values: Array<out T?>?) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotInWhenPresent(values))
 
-    infix fun <T> BindableColumn<T>.isNotInWhenPresent(values: Collection<T?>?) =
+    infix fun <T : Any> BindableColumn<T>.isNotInWhenPresent(values: Collection<T?>?) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotInWhenPresent(values))
 
-    infix fun <T> BindableColumn<T>.isBetween(value1: T & Any) =
-        SecondValueCollector<T & Any> {
+    infix fun <T : Any> BindableColumn<T>.isBetween(value1: T) =
+        SecondValueCollector<T> {
             invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isBetween(value1).and(it))
         }
 
-    infix fun <T> BindableColumn<T>.isBetweenWhenPresent(value1: T?) =
+    infix fun <T : Any> BindableColumn<T>.isBetweenWhenPresent(value1: T?) =
         NullableSecondValueCollector<T> {
             invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isBetweenWhenPresent(value1).and(it))
         }
 
-    infix fun <T> BindableColumn<T>.isNotBetween(value1: T & Any) =
-        SecondValueCollector<T & Any> {
+    infix fun <T : Any> BindableColumn<T>.isNotBetween(value1: T) =
+        SecondValueCollector<T> {
             invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotBetween(value1).and(it))
         }
 
-    infix fun <T> BindableColumn<T>.isNotBetweenWhenPresent(value1: T?) =
+    infix fun <T : Any> BindableColumn<T>.isNotBetweenWhenPresent(value1: T?) =
         NullableSecondValueCollector<T> {
             invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotBetweenWhenPresent(value1).and(it))
         }
 
     // for string columns, but generic for columns with type handlers
-    infix fun <T> BindableColumn<T>.isLike(value: T & Any) =
+    infix fun <T : Any> BindableColumn<T>.isLike(value: T) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLike(value))
 
-    infix fun <T> BindableColumn<T>.isLikeWhenPresent(value: T?) =
+    infix fun <T : Any> BindableColumn<T>.isLikeWhenPresent(value: T?) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLikeWhenPresent(value))
 
-    infix fun <T> BindableColumn<T>.isNotLike(value: T & Any) =
+    infix fun <T : Any> BindableColumn<T>.isNotLike(value: T) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotLike(value))
 
-    infix fun <T> BindableColumn<T>.isNotLikeWhenPresent(value: T?) =
+    infix fun <T : Any> BindableColumn<T>.isNotLikeWhenPresent(value: T?) =
         invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotLikeWhenPresent(value))
 
     // shortcuts for booleans
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 3ba5c2acb..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -16,40 +16,30 @@
 package org.mybatis.dynamic.sql.util.kotlin
 
 import org.mybatis.dynamic.sql.BindableColumn
+import org.mybatis.dynamic.sql.RenderableCondition
 import org.mybatis.dynamic.sql.SqlBuilder
-import org.mybatis.dynamic.sql.select.join.JoinCondition
-import org.mybatis.dynamic.sql.select.join.JoinCriterion
 
 typealias JoinReceiver = JoinCollector.() -> Unit
 
 @MyBatisDslMarker
 class JoinCollector {
-    private var onJoinCriterion: JoinCriterion<*>? = null
-    internal val andJoinCriteria = mutableListOf<JoinCriterion<*>>()
+    private val criteriaCollector = GroupingCriteriaCollector()
 
-    internal fun onJoinCriterion() : JoinCriterion<*> = invalidIfNull(onJoinCriterion, "ERROR.22") //$NON-NLS-1$
+    internal fun initialCriterion() = invalidIfNull(criteriaCollector.initialCriterion, "ERROR.22") //$NON-NLS-1$
+    internal fun subCriteria() = criteriaCollector.subCriteria
 
-    fun <T> on(leftColumn: BindableColumn<T>): RightColumnCollector<T> = RightColumnCollector {
-        onJoinCriterion = JoinCriterion.Builder<T>()
-            .withConnector("on") //$NON-NLS-1$
-            .withJoinColumn(leftColumn)
-            .withJoinCondition(it)
-            .build()
+    fun <T : Any> on(leftColumn: BindableColumn<T>): RightColumnCollector<T> = RightColumnCollector {
+        assertNull(criteriaCollector.initialCriterion, "ERROR.45") //$NON-NLS-1$
+        criteriaCollector.apply { leftColumn.invoke(it) }
     }
 
-    fun <T> and(leftColumn: BindableColumn<T>): RightColumnCollector<T> = RightColumnCollector {
-        andJoinCriteria.add(
-            JoinCriterion.Builder<T>()
-                .withConnector("and") //$NON-NLS-1$
-                .withJoinColumn(leftColumn)
-                .withJoinCondition(it)
-                .build()
-        )
+    fun <T : Any> and(leftColumn: BindableColumn<T>): RightColumnCollector<T> = RightColumnCollector {
+        criteriaCollector.and { leftColumn.invoke(it) }
     }
 }
 
-class RightColumnCollector<T>(private val joinConditionConsumer: (JoinCondition<T>) -> Unit) {
-    infix fun equalTo(rightColumn: BindableColumn<T>) = joinConditionConsumer.invoke(SqlBuilder.equalTo(rightColumn))
+class RightColumnCollector<T : Any>(private val joinConditionConsumer: (RenderableCondition<T>) -> Unit) {
+    infix fun equalTo(rightColumn: BindableColumn<T>) = joinConditionConsumer.invoke(SqlBuilder.isEqualTo(rightColumn))
 
-    infix fun equalTo(value: T) = joinConditionConsumer.invoke(SqlBuilder.equalTo(value))
+    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
index fee58915a..1203b555a 100644
--- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KInvalidSQLException.kt
+++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KInvalidSQLException.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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
index c50b856e1..f9b6aa323 100644
--- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KValidator.kt
+++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KValidator.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 2f6bc175b..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -25,15 +25,6 @@ import org.mybatis.dynamic.sql.where.AbstractWhereStarter
 @DslMarker
 annotation class MyBatisDslMarker
 
-@Deprecated("Please use GroupingCriteriaCollector.where")
-typealias WhereApplier = KotlinBaseBuilder<*>.() -> Unit
-
-@Deprecated("Please use GroupingCriteriaCollector.where")
-fun WhereApplier.andThen(after: WhereApplier): WhereApplier = {
-    invoke(this)
-    after(this)
-}
-
 @MyBatisDslMarker
 @Suppress("TooManyFunctions")
 abstract class KotlinBaseBuilder<D : AbstractWhereStarter<*,*>> {
@@ -51,31 +42,6 @@ abstract class KotlinBaseBuilder<D : AbstractWhereStarter<*,*>> {
         getDsl().where(criteria)
     }
 
-    @Deprecated("Please move the \"and\" function into the where lambda. If the where lambda has more than one condition, you may need to surround the existing conditions with \"group\" first.")
-    fun and(criteria: GroupingCriteriaReceiver): Unit =
-        GroupingCriteriaCollector().apply(criteria).let {
-            getDsl().where().and(it.initialCriterion, it.subCriteria)
-        }
-
-    @Deprecated("Please move the \"and\" function into the where lambda. If the where lambda has more than one condition, you may need to surround the existing conditions with \"group\" first.")
-    fun and(criteria: List<AndOrCriteriaGroup>) {
-        getDsl().where().and(criteria)
-    }
-
-    @Deprecated("Please move the \"or\" function into the where lambda. If the where lambda has more than one condition, you may need to surround the existing conditions with \"group\" first.")
-    fun or(criteria: GroupingCriteriaReceiver): Unit =
-        GroupingCriteriaCollector().apply(criteria).let {
-            getDsl().where().or(it.initialCriterion, it.subCriteria)
-        }
-
-    @Deprecated("Please move the \"or\" function into the where lambda. If the where lambda has more than one condition, you may need to surround the existing conditions with \"group\" first.")
-    fun or(criteria: List<AndOrCriteriaGroup>) {
-        getDsl().where().or(criteria)
-    }
-
-    @Deprecated("Please use GroupingCriteriaCollector.where, then pass it to the \"where\" method")
-    fun applyWhere(whereApplier: WhereApplier) = whereApplier.invoke(this)
-
     /**
      * This function does nothing, but it can be used to make some code snippets more understandable.
      *
@@ -98,76 +64,156 @@ abstract class KotlinBaseBuilder<D : AbstractWhereStarter<*,*>> {
 @Suppress("TooManyFunctions")
 abstract class KotlinBaseJoiningBuilder<D : AbstractQueryExpressionDSL<*, *>> : KotlinBaseBuilder<D>() {
 
+    @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.onJoinCriterion(), jc.andJoinCriteria)
+            join(table, jc.initialCriterion(), jc.subCriteria())
         }
 
+    @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.onJoinCriterion(), jc.andJoinCriteria)
+            join(table, alias, jc.initialCriterion(), jc.subCriteria())
         }
 
+    @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.onJoinCriterion(), jc.andJoinCriteria)
+            join(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria())
+        }
+
+    fun join(table: SqlTable): JoinCriteriaGatherer =
+        JoinCriteriaGatherer {
+            getDsl().join(table, it.initialCriterion, it.subCriteria)
+        }
+
+    fun join(table: SqlTable, alias: String): JoinCriteriaGatherer =
+        JoinCriteriaGatherer {
+            getDsl().join(table, alias, it.initialCriterion, it.subCriteria)
         }
 
+    fun join(
+        subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit): JoinCriteriaGatherer =
+        JoinCriteriaGatherer {
+            val sq = KotlinQualifiedSubQueryBuilder().apply(subQuery)
+            getDsl().join(sq, sq.correlationName, it.initialCriterion, it.subCriteria)
+        }
+
+    @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.onJoinCriterion(), jc.andJoinCriteria)
+            fullJoin(table, jc.initialCriterion(), jc.subCriteria())
         }
 
+    @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.onJoinCriterion(), jc.andJoinCriteria)
+            fullJoin(table, alias, jc.initialCriterion(), jc.subCriteria())
         }
 
+    @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.onJoinCriterion(), jc.andJoinCriteria)
+            fullJoin(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria())
+        }
+
+    fun fullJoin(table: SqlTable): JoinCriteriaGatherer =
+        JoinCriteriaGatherer {
+            getDsl().fullJoin(table, it.initialCriterion, it.subCriteria)
         }
 
+    fun fullJoin(table: SqlTable, alias: String): JoinCriteriaGatherer =
+        JoinCriteriaGatherer {
+            getDsl().fullJoin(table, alias, it.initialCriterion, it.subCriteria)
+        }
+
+    fun fullJoin(
+        subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit): JoinCriteriaGatherer =
+        JoinCriteriaGatherer {
+            val sq = KotlinQualifiedSubQueryBuilder().apply(subQuery)
+            getDsl().fullJoin(sq, sq.correlationName, it.initialCriterion, it.subCriteria)
+        }
+
+    @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.onJoinCriterion(), jc.andJoinCriteria)
+            leftJoin(table, jc.initialCriterion(), jc.subCriteria())
         }
 
+    @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.onJoinCriterion(), jc.andJoinCriteria)
+            leftJoin(table, alias, jc.initialCriterion(), jc.subCriteria())
         }
 
+    @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.onJoinCriterion(), jc.andJoinCriteria)
+            leftJoin(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria())
+        }
+
+    fun leftJoin(table: SqlTable): JoinCriteriaGatherer =
+        JoinCriteriaGatherer {
+            getDsl().leftJoin(table, it.initialCriterion, it.subCriteria)
+        }
+
+    fun leftJoin(table: SqlTable, alias: String): JoinCriteriaGatherer =
+        JoinCriteriaGatherer {
+            getDsl().leftJoin(table, alias, it.initialCriterion, it.subCriteria)
+        }
+
+    fun leftJoin(
+        subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit): JoinCriteriaGatherer =
+        JoinCriteriaGatherer {
+            val sq = KotlinQualifiedSubQueryBuilder().apply(subQuery)
+            getDsl().leftJoin(sq, sq.correlationName, it.initialCriterion, it.subCriteria)
         }
 
+    @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.onJoinCriterion(), jc.andJoinCriteria)
+            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.onJoinCriterion(), jc.andJoinCriteria)
+            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.onJoinCriterion(), jc.andJoinCriteria)
+            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) {
@@ -182,3 +228,11 @@ abstract class KotlinBaseJoiningBuilder<D : AbstractQueryExpressionDSL<*, *>> :
         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
index 65ae006da..aa2dd3703 100644
--- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBatchInsertBuilder.kt
+++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBatchInsertBuilder.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -33,7 +33,7 @@ class KotlinBatchInsertBuilder<T : Any> (private val rows: Collection<T>): Build
         this.table = table
     }
 
-    fun <C> map(column: SqlColumn<C>) = MultiRowInsertColumnMapCompleter(column) {
+    fun <C : Any> map(column: SqlColumn<C>) = MultiRowInsertColumnMapCompleter(column) {
         columnMappings.add(it)
     }
 
@@ -41,7 +41,7 @@ class KotlinBatchInsertBuilder<T : Any> (private val rows: Collection<T>): Build
         assertNotNull(table, "ERROR.23") //$NON-NLS-1$
         return with(BatchInsertDSL.Builder<T>()) {
             withRecords(rows)
-            withTable(table)
+            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 78f1f880e..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 011928b9e..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -30,7 +30,11 @@ class KotlinDeleteBuilder(private val dsl: DeleteDSL<DeleteModel>) :
     }
 
     fun limit(limit: Long) {
-        dsl.limit(limit)
+        limitWhenPresent(limit)
+    }
+
+    fun limitWhenPresent(limit: Long?) {
+        dsl.limitWhenPresent(limit)
     }
 
     override fun build(): DeleteModel = dsl.build()
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
index 152aace24..413ddcac4 100644
--- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinGeneralInsertBuilder.kt
+++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinGeneralInsertBuilder.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -29,7 +29,7 @@ class KotlinGeneralInsertBuilder(private val table: SqlTable) : Buildable<Genera
 
     private val columnMappings = mutableListOf<AbstractColumnMapping>()
 
-    fun <T> set(column: SqlColumn<T>) = GeneralInsertColumnSetCompleter(column) {
+    fun <T : Any> set(column: SqlColumn<T>) = GeneralInsertColumnSetCompleter(column) {
         columnMappings.add(it)
     }
 
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
index cb0730575..8b76231f7 100644
--- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinInsertBuilder.kt
+++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinInsertBuilder.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -33,7 +33,7 @@ class KotlinInsertBuilder<T : Any> (private val row: T): Buildable<InsertModel<T
         this.table = table
     }
 
-    fun <C> map(column: SqlColumn<C>) = SingleRowInsertColumnMapCompleter(column) {
+    fun <C : Any> map(column: SqlColumn<C>) = SingleRowInsertColumnMapCompleter(column) {
         columnMappings.add(it)
     }
 
@@ -41,7 +41,7 @@ class KotlinInsertBuilder<T : Any> (private val row: T): Buildable<InsertModel<T
         assertNotNull(table, "ERROR.25") //$NON-NLS-1$
         return with(InsertDSL.Builder<T>()) {
             withRow(row)
-            withTable(table)
+            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
index dd995e23c..076001b9e 100644
--- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinInsertColumnMapCompleters.kt
+++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinInsertColumnMapCompleters.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -28,7 +28,7 @@ import org.mybatis.dynamic.sql.util.ValueOrNullMapping
 import org.mybatis.dynamic.sql.util.ValueWhenPresentMapping
 
 @MyBatisDslMarker
-sealed class AbstractInsertColumnMapCompleter<T>(
+sealed class AbstractInsertColumnMapCompleter<T : Any>(
     internal val column: SqlColumn<T>,
     internal val mappingConsumer: (AbstractColumnMapping) -> Unit) {
 
@@ -39,7 +39,7 @@ sealed class AbstractInsertColumnMapCompleter<T>(
     infix fun toStringConstant(constant: String) = mappingConsumer.invoke(StringConstantMapping.of(column, constant))
 }
 
-class MultiRowInsertColumnMapCompleter<T>(
+class MultiRowInsertColumnMapCompleter<T : Any>(
     column: SqlColumn<T>,
     mappingConsumer: (AbstractColumnMapping) -> Unit)
     : AbstractInsertColumnMapCompleter<T>(column, mappingConsumer) {
@@ -49,7 +49,7 @@ class MultiRowInsertColumnMapCompleter<T>(
     fun toRow() = mappingConsumer.invoke(RowMapping.of(column))
 }
 
-class SingleRowInsertColumnMapCompleter<T>(
+class SingleRowInsertColumnMapCompleter<T : Any>(
     column: SqlColumn<T>,
     mappingConsumer: (AbstractColumnMapping) -> Unit)
     : AbstractInsertColumnMapCompleter<T>(column, mappingConsumer) {
@@ -62,14 +62,14 @@ class SingleRowInsertColumnMapCompleter<T>(
     fun toRow() = mappingConsumer.invoke(RowMapping.of(column))
 }
 
-class GeneralInsertColumnSetCompleter<T>(
+class GeneralInsertColumnSetCompleter<T : Any>(
     column: SqlColumn<T>,
     mappingConsumer: (AbstractColumnMapping) -> Unit)
     : AbstractInsertColumnMapCompleter<T>(column, mappingConsumer) {
 
-    infix fun toValue(value: T & Any) = toValue { value }
+    infix fun toValue(value: T) = toValue { value }
 
-    infix fun toValue(value: () -> T & Any) = mappingConsumer.invoke(ValueMapping.of(column, value))
+    infix fun toValue(value: () -> T) = mappingConsumer.invoke(ValueMapping.of(column, value))
 
     infix fun toValueOrNull(value: T?) = toValueOrNull { 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
index e8049c4ef..b00a62aef 100644
--- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinMultiRowInsertBuilder.kt
+++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinMultiRowInsertBuilder.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -33,7 +33,7 @@ class KotlinMultiRowInsertBuilder<T : Any> (private val rows: Collection<T>): Bu
         this.table = table
     }
 
-    fun <C> map(column: SqlColumn<C>) = MultiRowInsertColumnMapCompleter(column) {
+    fun <C : Any> map(column: SqlColumn<C>) = MultiRowInsertColumnMapCompleter(column) {
         columnMappings.add(it)
     }
 
@@ -41,7 +41,7 @@ class KotlinMultiRowInsertBuilder<T : Any> (private val rows: Collection<T>): Bu
         assertNotNull(table, "ERROR.26") //$NON-NLS-1$
         return with(MultiRowInsertDSL.Builder<T>()) {
             withRecords(rows)
-            withTable(table)
+            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
index 440232397..618909cd2 100644
--- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinMultiSelectBuilder.kt
+++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinMultiSelectBuilder.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -26,9 +26,9 @@ import org.mybatis.dynamic.sql.util.Buildable
 typealias MultiSelectCompleter = KotlinMultiSelectBuilder.() -> Unit
 
 @MyBatisDslMarker
-class KotlinMultiSelectBuilder: Buildable<MultiSelectModel> {
+class KotlinMultiSelectBuilder: Buildable<MultiSelectModel>, KotlinPagingDSL {
     private var dsl: MultiSelectDSL? = null
-        private set(value) {
+        set(value) {
             assertNull(field, "ERROR.33") //$NON-NLS-1$
             field = value
         }
@@ -63,16 +63,16 @@ class KotlinMultiSelectBuilder: Buildable<MultiSelectModel> {
         getDsl().orderBy(columns.asList())
     }
 
-    fun limit(limit: Long) {
-        getDsl().limit(limit)
+    override fun limitWhenPresent(limit: Long?) {
+        getDsl().limitWhenPresent(limit)
     }
 
-    fun offset(offset: Long) {
-        getDsl().offset(offset)
+    override fun offsetWhenPresent(offset: Long?) {
+        getDsl().offsetWhenPresent(offset)
     }
 
-    fun fetchFirst(fetchFirstRows: Long) {
-        getDsl().fetchFirst(fetchFirstRows).rowsOnly()
+    override fun fetchFirstWhenPresent(fetchFirstRows: Long?) {
+        getDsl().fetchFirstWhenPresent(fetchFirstRows).rowsOnly()
     }
 
     fun configureStatement(c: StatementConfiguration.() -> Unit) {
diff --git a/src/test/java/examples/schema_supplier/User.java b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinPagingDSL.kt
similarity index 56%
rename from src/test/java/examples/schema_supplier/User.java
rename to src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinPagingDSL.kt
index 02acc9c53..03dd8082f 100644
--- a/src/test/java/examples/schema_supplier/User.java
+++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinPagingDSL.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -13,25 +13,24 @@
  *    See the License for the specific language governing permissions and
  *    limitations under the License.
  */
-package examples.schema_supplier;
+package org.mybatis.dynamic.sql.util.kotlin
 
-public class User {
-    private int id;
-    private String name;
-
-    public int getId() {
-        return id;
+interface KotlinPagingDSL {
+    fun limit(limit: Long) {
+        limitWhenPresent(limit)
     }
 
-    public void setId(int id) {
-        this.id = id;
-    }
+    fun limitWhenPresent(limit: Long?)
 
-    public String getName() {
-        return name;
+    fun offset(offset: Long) {
+        offsetWhenPresent(offset)
     }
 
-    public void setName(String name) {
-        this.name = name;
+    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/KotlinSelectBuilder.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinSelectBuilder.kt
index e62b5ef26..4c80f9d8c 100644
--- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinSelectBuilder.kt
+++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinSelectBuilder.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -28,7 +28,7 @@ typealias SelectCompleter = KotlinSelectBuilder.() -> Unit
 
 @Suppress("TooManyFunctions")
 class KotlinSelectBuilder(private val fromGatherer: QueryExpressionDSL.FromGatherer<SelectModel>) :
-    KotlinBaseJoiningBuilder<QueryExpressionDSL<SelectModel>>(), Buildable<SelectModel> {
+    KotlinBaseJoiningBuilder<QueryExpressionDSL<SelectModel>>(), Buildable<SelectModel>, KotlinPagingDSL {
 
     private var dsl: KQueryExpressionDSL? = null
 
@@ -58,16 +58,16 @@ class KotlinSelectBuilder(private val fromGatherer: QueryExpressionDSL.FromGathe
         getDsl().orderBy(columns.toList())
     }
 
-    fun limit(limit: Long) {
-        getDsl().limit(limit)
+    override fun limitWhenPresent(limit: Long?) {
+        getDsl().limitWhenPresent(limit)
     }
 
-    fun offset(offset: Long) {
-        getDsl().offset(offset)
+    override fun offsetWhenPresent(offset: Long?) {
+        getDsl().offsetWhenPresent(offset)
     }
 
-    fun fetchFirst(fetchFirstRows: Long) {
-        getDsl().fetchFirst(fetchFirstRows).rowsOnly()
+    override fun fetchFirstWhenPresent(fetchFirstRows: Long?) {
+        getDsl().fetchFirstWhenPresent(fetchFirstRows).rowsOnly()
     }
 
     fun union(union: KotlinUnionBuilder.() -> Unit): Unit =
@@ -76,6 +76,30 @@ class KotlinSelectBuilder(private val fromGatherer: QueryExpressionDSL.FromGathe
     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$
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
index cf5a12d8a..9439e4058 100644
--- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinSubQueryBuilders.kt
+++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinSubQueryBuilders.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -84,11 +84,11 @@ class KotlinInsertSelectSubQueryBuilder : KotlinBaseSubQueryBuilder(), Buildable
         assertNotNull(table, "ERROR.29") //$NON-NLS-1$
 
         val dsl = if (columnList == null) {
-            SqlBuilder.insertInto(table)
+            SqlBuilder.insertInto(table!!)
                 .withSelectStatement { buildSelectModel() }
         } else {
-            SqlBuilder.insertInto(table)
-                .withColumnList(columnList)
+            SqlBuilder.insertInto(table!!)
+                .withColumnList(columnList!!)
                 .withSelectStatement { buildSelectModel() }
         }
 
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
index 817dba6e8..b403d675f 100644
--- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinUnionBuilder.kt
+++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinUnionBuilder.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 ef7eb6804..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -27,14 +27,18 @@ typealias UpdateCompleter = KotlinUpdateBuilder.() -> Unit
 class KotlinUpdateBuilder(private val dsl: UpdateDSL<UpdateModel>) :
     KotlinBaseBuilder<UpdateDSL<UpdateModel>>(), Buildable<UpdateModel> {
 
-    fun <T> set(column: SqlColumn<T>): KotlinSetClauseFinisher<T> = KotlinSetClauseFinisher(column)
+    fun <T : Any> set(column: SqlColumn<T>): KotlinSetClauseFinisher<T> = KotlinSetClauseFinisher(column)
 
     fun orderBy(vararg columns: SortSpecification) {
         dsl.orderBy(columns.toList())
     }
 
     fun limit(limit: Long) {
-        dsl.limit(limit)
+        limitWhenPresent(limit)
+    }
+
+    fun limitWhenPresent(limit: Long?) {
+        dsl.limitWhenPresent(limit)
     }
 
     override fun build(): UpdateModel = dsl.build()
@@ -43,7 +47,7 @@ class KotlinUpdateBuilder(private val dsl: UpdateDSL<UpdateModel>) :
 
     @MyBatisDslMarker
     @Suppress("TooManyFunctions")
-    inner class KotlinSetClauseFinisher<T>(private val column: SqlColumn<T>) {
+    inner class KotlinSetClauseFinisher<T : Any>(private val column: SqlColumn<T>) {
         fun equalToNull(): Unit =
             applyToDsl {
                 set(column).equalToNull()
@@ -59,9 +63,9 @@ class KotlinUpdateBuilder(private val dsl: UpdateDSL<UpdateModel>) :
                 set(column).equalToStringConstant(constant)
             }
 
-        infix fun equalTo(value: T & Any): Unit = equalTo { value }
+        infix fun equalTo(value: T): Unit = equalTo { value }
 
-        infix fun equalTo(value: () -> T & Any): Unit =
+        infix fun equalTo(value: () -> T): Unit =
             applyToDsl {
                 set(column).equalTo(value)
             }
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
index 639a981fe..ce33f27b7 100644
--- 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
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -16,12 +16,13 @@
 package org.mybatis.dynamic.sql.util.kotlin.elements
 
 import org.mybatis.dynamic.sql.BasicColumn
-import org.mybatis.dynamic.sql.VisitableCondition
+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 {
@@ -34,9 +35,10 @@ class KSearchedCaseDSL : KElseDSL {
 
     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)
+                .withThenValue(thenValue!!)
                 .build())
         }
 
@@ -65,7 +67,7 @@ class KSimpleCaseDSL<T : Any> : KElseDSL {
         }
     internal val whenConditions = mutableListOf<SimpleCaseWhenCondition<T>>()
 
-    fun `when`(vararg conditions: VisitableCondition<T>) =
+    fun `when`(vararg conditions: RenderableCondition<T>) =
         SimpleCaseThenGatherer { whenConditions.add(ConditionBasedWhenCondition(conditions.asList(), it)) }
 
     fun `when`(vararg values: T) =
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
index cebf26ee9..836c61443 100644
--- 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
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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
index fc4e2a104..ab5f81a2a 100644
--- 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
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -20,17 +20,17 @@ import org.mybatis.dynamic.sql.SqlColumn
 import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseModel
 import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseModel
 
-infix fun <T> DerivedColumn<T>.`as`(alias: String): DerivedColumn<T> = this.`as`(alias)
+infix fun <T : Any> DerivedColumn<T>.`as`(alias: String): DerivedColumn<T> = this.`as`(alias)
 
-infix fun <T> SqlColumn<T>.`as`(alias: String): SqlColumn<T> = this.`as`(alias)
+infix fun <T : Any> SqlColumn<T>.`as`(alias: String): SqlColumn<T> = this.`as`(alias)
 
 infix fun SearchedCaseModel.`as`(alias: String): SearchedCaseModel = this.`as`(alias)
 
-infix fun <T> SimpleCaseModel<T>.`as`(alias: String): SimpleCaseModel<T> = this.`as`(alias)
+infix fun <T : Any> SimpleCaseModel<T>.`as`(alias: String): SimpleCaseModel<T> = 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 <T> String.invoke(column: SqlColumn<T>): SqlColumn<T> = column.qualifiedWith(this)
+operator fun <T : Any> String.invoke(column: SqlColumn<T>): SqlColumn<T> = 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
index 2d9f1e8a1..f648496d0 100644
--- 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
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -21,11 +21,11 @@ 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.VisitableCondition
 import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseModel
 import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseModel
 import org.mybatis.dynamic.sql.select.aggregate.Avg
@@ -51,14 +51,18 @@ 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
@@ -69,13 +73,19 @@ 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
@@ -84,6 +94,8 @@ 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
 
@@ -129,43 +141,45 @@ fun count(column: BasicColumn): Count = SqlBuilder.count(column)
 
 fun countDistinct(column: BasicColumn): CountDistinct = SqlBuilder.countDistinct(column)
 
-fun <T> max(column: BindableColumn<T>): Max<T> = SqlBuilder.max(column)
+fun <T : Any> max(column: BindableColumn<T>): Max<T> = SqlBuilder.max(column)
 
-fun <T> min(column: BindableColumn<T>): Min<T> = SqlBuilder.min(column)
+fun <T : Any> min(column: BindableColumn<T>): Min<T> = SqlBuilder.min(column)
 
-fun <T> avg(column: BindableColumn<T>): Avg<T> = SqlBuilder.avg(column)
+fun <T : Any> avg(column: BindableColumn<T>): Avg<T> = SqlBuilder.avg(column)
 
-fun <T> sum(column: BindableColumn<T>): Sum<T> = SqlBuilder.sum(column)
+fun <T : Any> sum(column: BindableColumn<T>): Sum<T> = SqlBuilder.sum(column)
 
-fun <T> sum(column: BindableColumn<T>, condition: VisitableCondition<T>): Sum<T> = SqlBuilder.sum(column, condition)
+fun sum(column: BasicColumn): Sum<*> = SqlBuilder.sum(column)
+
+fun <T : Any> sum(column: BindableColumn<T>, condition: RenderableCondition<T>): Sum<T> = SqlBuilder.sum(column, condition)
 
 // constants
-fun <T> constant(constant: String): Constant<T> = SqlBuilder.constant(constant)
+fun <T : Any> constant(constant: String): Constant<T> = SqlBuilder.constant(constant)
 
 fun stringConstant(constant: String): StringConstant = SqlBuilder.stringConstant(constant)
 
-fun <T> value(value: T): BoundValue<T> = SqlBuilder.value(value)
+fun <T : Any> value(value: T): BoundValue<T> = SqlBuilder.value(value)
 
 // functions
-fun <T> add(
+fun <T : Any> add(
     firstColumn: BindableColumn<T>,
     secondColumn: BasicColumn,
     vararg subsequentColumns: BasicColumn
 ): Add<T> = Add.of(firstColumn, secondColumn, subsequentColumns.asList())
 
-fun <T> divide(
+fun <T : Any> divide(
     firstColumn: BindableColumn<T>,
     secondColumn: BasicColumn,
     vararg subsequentColumns: BasicColumn
 ): Divide<T> = Divide.of(firstColumn, secondColumn, subsequentColumns.asList())
 
-fun <T> multiply(
+fun <T : Any> multiply(
     firstColumn: BindableColumn<T>,
     secondColumn: BasicColumn,
     vararg subsequentColumns: BasicColumn
 ): Multiply<T> = Multiply.of(firstColumn, secondColumn, subsequentColumns.asList())
 
-fun <T> subtract(
+fun <T : Any> subtract(
     firstColumn: BindableColumn<T>,
     secondColumn: BasicColumn,
     vararg subsequentColumns: BasicColumn
@@ -174,147 +188,149 @@ fun <T> subtract(
 fun cast(receiver: CastDSL.() -> Unit): Cast =
     invalidIfNull(CastDSL().apply(receiver).cast, "ERROR.43")
 
-fun <T> concat(
+fun <T : Any> concat(
     firstColumn: BindableColumn<T>,
     vararg subsequentColumns: BasicColumn
 ): Concat<T> = Concat.of(firstColumn, subsequentColumns.asList())
 
-fun <T> concatenate(
+fun <T : Any> concatenate(
     firstColumn: BindableColumn<T>,
     secondColumn: BasicColumn,
     vararg subsequentColumns: BasicColumn
 ): Concatenate<T> = Concatenate.of(firstColumn, secondColumn, subsequentColumns.asList())
 
-fun <T> applyOperator(
+fun <T : Any> applyOperator(
     operator: String,
     firstColumn: BindableColumn<T>,
     secondColumn: BasicColumn,
     vararg subsequentColumns: BasicColumn
 ): OperatorFunction<T> = OperatorFunction.of(operator, firstColumn, secondColumn, subsequentColumns.asList())
 
-fun <T> lower(column: BindableColumn<T>): Lower<T> = SqlBuilder.lower(column)
+fun <T : Any> lower(column: BindableColumn<T>): Lower<T> = SqlBuilder.lower(column)
 
-fun <T> substring(
+fun <T : Any> substring(
     column: BindableColumn<T>,
     offset: Int,
     length: Int
 ): Substring<T> = SqlBuilder.substring(column, offset, length)
 
-fun <T> upper(column: BindableColumn<T>): Upper<T> = SqlBuilder.upper(column)
+fun <T : Any> upper(column: BindableColumn<T>): Upper<T> = SqlBuilder.upper(column)
 
 // conditions for all data types
-fun <T> isNull(): IsNull<T> = SqlBuilder.isNull()
+fun <T : Any> isNull(): IsNull<T> = SqlBuilder.isNull()
 
-fun <T> isNotNull(): IsNotNull<T> = SqlBuilder.isNotNull()
+fun <T : Any> isNotNull(): IsNotNull<T> = SqlBuilder.isNotNull()
 
-fun <T> isEqualTo(value: T & Any): IsEqualTo<T> = SqlBuilder.isEqualTo(value)
+fun <T : Any> isEqualTo(value: T): IsEqualTo<T> = SqlBuilder.isEqualTo(value)
 
-fun <T> isEqualTo(subQuery: KotlinSubQueryBuilder.() -> Unit): IsEqualToWithSubselect<T> =
+fun <T : Any> isEqualTo(subQuery: KotlinSubQueryBuilder.() -> Unit): IsEqualToWithSubselect<T> =
     SqlBuilder.isEqualTo(KotlinSubQueryBuilder().apply(subQuery))
 
-fun <T> isEqualTo(column: BasicColumn): IsEqualToColumn<T> = SqlBuilder.isEqualTo(column)
+fun <T : Any> isEqualTo(column: BasicColumn): IsEqualToColumn<T> = SqlBuilder.isEqualTo(column)
 
-fun <T> isEqualToWhenPresent(value: T?): IsEqualTo<T> = SqlBuilder.isEqualToWhenPresent(value)
+fun <T : Any> isEqualToWhenPresent(value: T?): IsEqualToWhenPresent<T> = SqlBuilder.isEqualToWhenPresent(value)
 
-fun <T> isNotEqualTo(value: T & Any): IsNotEqualTo<T> = SqlBuilder.isNotEqualTo(value)
+fun <T : Any> isNotEqualTo(value: T): IsNotEqualTo<T> = SqlBuilder.isNotEqualTo(value)
 
-fun <T> isNotEqualTo(subQuery: KotlinSubQueryBuilder.() -> Unit): IsNotEqualToWithSubselect<T> =
+fun <T : Any> isNotEqualTo(subQuery: KotlinSubQueryBuilder.() -> Unit): IsNotEqualToWithSubselect<T> =
     SqlBuilder.isNotEqualTo(KotlinSubQueryBuilder().apply(subQuery))
 
-fun <T> isNotEqualTo(column: BasicColumn): IsNotEqualToColumn<T> = SqlBuilder.isNotEqualTo(column)
+fun <T : Any> isNotEqualTo(column: BasicColumn): IsNotEqualToColumn<T> = SqlBuilder.isNotEqualTo(column)
 
-fun <T> isNotEqualToWhenPresent(value: T?): IsNotEqualTo<T> = SqlBuilder.isNotEqualToWhenPresent(value)
+fun <T : Any> isNotEqualToWhenPresent(value: T?): IsNotEqualToWhenPresent<T> =
+    SqlBuilder.isNotEqualToWhenPresent(value)
 
-fun <T> isGreaterThan(value: T & Any): IsGreaterThan<T> = SqlBuilder.isGreaterThan(value)
+fun <T : Any> isGreaterThan(value: T): IsGreaterThan<T> = SqlBuilder.isGreaterThan(value)
 
-fun <T> isGreaterThan(subQuery: KotlinSubQueryBuilder.() -> Unit): IsGreaterThanWithSubselect<T> =
+fun <T : Any> isGreaterThan(subQuery: KotlinSubQueryBuilder.() -> Unit): IsGreaterThanWithSubselect<T> =
     SqlBuilder.isGreaterThan(KotlinSubQueryBuilder().apply(subQuery))
 
-fun <T> isGreaterThan(column: BasicColumn): IsGreaterThanColumn<T> = SqlBuilder.isGreaterThan(column)
+fun <T : Any> isGreaterThan(column: BasicColumn): IsGreaterThanColumn<T> = SqlBuilder.isGreaterThan(column)
 
-fun <T> isGreaterThanWhenPresent(value: T?): IsGreaterThan<T> = SqlBuilder.isGreaterThanWhenPresent(value)
+fun <T : Any> isGreaterThanWhenPresent(value: T?): IsGreaterThanWhenPresent<T> =
+    SqlBuilder.isGreaterThanWhenPresent(value)
 
-fun <T> isGreaterThanOrEqualTo(value: T & Any): IsGreaterThanOrEqualTo<T> = SqlBuilder.isGreaterThanOrEqualTo(value)
+fun <T : Any> isGreaterThanOrEqualTo(value: T): IsGreaterThanOrEqualTo<T> = SqlBuilder.isGreaterThanOrEqualTo(value)
 
-fun <T> isGreaterThanOrEqualTo(subQuery: KotlinSubQueryBuilder.() -> Unit): IsGreaterThanOrEqualToWithSubselect<T> =
+fun <T : Any> isGreaterThanOrEqualTo(subQuery: KotlinSubQueryBuilder.() -> Unit): IsGreaterThanOrEqualToWithSubselect<T> =
     SqlBuilder.isGreaterThanOrEqualTo(KotlinSubQueryBuilder().apply(subQuery))
 
-fun <T> isGreaterThanOrEqualTo(column: BasicColumn): IsGreaterThanOrEqualToColumn<T> =
+fun <T : Any> isGreaterThanOrEqualTo(column: BasicColumn): IsGreaterThanOrEqualToColumn<T> =
     SqlBuilder.isGreaterThanOrEqualTo(column)
 
-fun <T> isGreaterThanOrEqualToWhenPresent(value: T?): IsGreaterThanOrEqualTo<T> =
+fun <T : Any> isGreaterThanOrEqualToWhenPresent(value: T?): IsGreaterThanOrEqualToWhenPresent<T> =
     SqlBuilder.isGreaterThanOrEqualToWhenPresent(value)
 
-fun <T> isLessThan(value: T & Any): IsLessThan<T> = SqlBuilder.isLessThan(value)
+fun <T : Any> isLessThan(value: T): IsLessThan<T> = SqlBuilder.isLessThan(value)
 
-fun <T> isLessThan(subQuery: KotlinSubQueryBuilder.() -> Unit): IsLessThanWithSubselect<T> =
+fun <T : Any> isLessThan(subQuery: KotlinSubQueryBuilder.() -> Unit): IsLessThanWithSubselect<T> =
     SqlBuilder.isLessThan(KotlinSubQueryBuilder().apply(subQuery))
 
-fun <T> isLessThan(column: BasicColumn): IsLessThanColumn<T> = SqlBuilder.isLessThan(column)
+fun <T : Any> isLessThan(column: BasicColumn): IsLessThanColumn<T> = SqlBuilder.isLessThan(column)
 
-fun <T> isLessThanWhenPresent(value: T?): IsLessThan<T> = SqlBuilder.isLessThanWhenPresent(value)
+fun <T : Any> isLessThanWhenPresent(value: T?): IsLessThanWhenPresent<T> = SqlBuilder.isLessThanWhenPresent(value)
 
-fun <T> isLessThanOrEqualTo(value: T & Any): IsLessThanOrEqualTo<T> = SqlBuilder.isLessThanOrEqualTo(value)
+fun <T : Any> isLessThanOrEqualTo(value: T): IsLessThanOrEqualTo<T> = SqlBuilder.isLessThanOrEqualTo(value)
 
-fun <T> isLessThanOrEqualTo(subQuery: KotlinSubQueryBuilder.() -> Unit): IsLessThanOrEqualToWithSubselect<T> =
+fun <T : Any> isLessThanOrEqualTo(subQuery: KotlinSubQueryBuilder.() -> Unit): IsLessThanOrEqualToWithSubselect<T> =
     SqlBuilder.isLessThanOrEqualTo(KotlinSubQueryBuilder().apply(subQuery))
 
-fun <T> isLessThanOrEqualTo(column: BasicColumn): IsLessThanOrEqualToColumn<T> = SqlBuilder.isLessThanOrEqualTo(column)
+fun <T : Any> isLessThanOrEqualTo(column: BasicColumn): IsLessThanOrEqualToColumn<T> = SqlBuilder.isLessThanOrEqualTo(column)
 
-fun <T> isLessThanOrEqualToWhenPresent(value: T?): IsLessThanOrEqualTo<T> =
+fun <T : Any> isLessThanOrEqualToWhenPresent(value: T?): IsLessThanOrEqualToWhenPresent<T> =
     SqlBuilder.isLessThanOrEqualToWhenPresent(value)
 
-fun <T> isIn(vararg values: T & Any): IsIn<T> = isIn(values.asList())
+fun <T : Any> isIn(vararg values: T): IsIn<T> = isIn(values.asList())
 
 @JvmName("isInArray")
-fun <T> isIn(values: Array<out T & Any>): IsIn<T> = SqlBuilder.isIn(values.asList())
+fun <T : Any> isIn(values: Array<out T>): IsIn<T> = SqlBuilder.isIn(values.asList())
 
-fun <T> isIn(values: Collection<T & Any>): IsIn<T> = SqlBuilder.isIn(values)
+fun <T : Any> isIn(values: Collection<T>): IsIn<T> = SqlBuilder.isIn(values)
 
-fun <T> isIn(subQuery: KotlinSubQueryBuilder.() -> Unit): IsInWithSubselect<T> =
+fun <T : Any> isIn(subQuery: KotlinSubQueryBuilder.() -> Unit): IsInWithSubselect<T> =
     SqlBuilder.isIn(KotlinSubQueryBuilder().apply(subQuery))
 
-fun <T> isInWhenPresent(vararg values: T?): IsInWhenPresent<T> = isInWhenPresent(values.asList())
+fun <T : Any> isInWhenPresent(vararg values: T?): IsInWhenPresent<T> = isInWhenPresent(values.asList())
 
 @JvmName("isInArrayWhenPresent")
-fun <T> isInWhenPresent(values: Array<out T?>?): IsInWhenPresent<T> = SqlBuilder.isInWhenPresent(values?.asList())
+fun <T : Any> isInWhenPresent(values: Array<out T?>?): IsInWhenPresent<T> = SqlBuilder.isInWhenPresent(values?.asList())
 
-fun <T> isInWhenPresent(values: Collection<T?>?): IsInWhenPresent<T> = SqlBuilder.isInWhenPresent(values)
+fun <T : Any> isInWhenPresent(values: Collection<T?>?): IsInWhenPresent<T> = SqlBuilder.isInWhenPresent(values)
 
-fun <T> isNotIn(vararg values: T & Any): IsNotIn<T> = isNotIn(values.asList())
+fun <T : Any> isNotIn(vararg values: T): IsNotIn<T> = isNotIn(values.asList())
 
 @JvmName("isNotInArray")
-fun <T> isNotIn(values: Array<out T & Any>): IsNotIn<T> = SqlBuilder.isNotIn(values.asList())
+fun <T : Any> isNotIn(values: Array<out T>): IsNotIn<T> = SqlBuilder.isNotIn(values.asList())
 
-fun <T> isNotIn(values: Collection<T & Any>): IsNotIn<T> = SqlBuilder.isNotIn(values)
+fun <T : Any> isNotIn(values: Collection<T>): IsNotIn<T> = SqlBuilder.isNotIn(values)
 
-fun <T> isNotIn(subQuery: KotlinSubQueryBuilder.() -> Unit): IsNotInWithSubselect<T> =
+fun <T : Any> isNotIn(subQuery: KotlinSubQueryBuilder.() -> Unit): IsNotInWithSubselect<T> =
     SqlBuilder.isNotIn(KotlinSubQueryBuilder().apply(subQuery))
 
-fun <T> isNotInWhenPresent(vararg values: T?): IsNotInWhenPresent<T> = isNotInWhenPresent(values.asList())
+fun <T : Any> isNotInWhenPresent(vararg values: T?): IsNotInWhenPresent<T> = isNotInWhenPresent(values.asList())
 
 @JvmName("isNotInArrayWhenPresent")
-fun <T> isNotInWhenPresent(values: Array<out T?>?): IsNotInWhenPresent<T> = SqlBuilder.isNotInWhenPresent(values?.asList())
+fun <T : Any> isNotInWhenPresent(values: Array<out T?>?): IsNotInWhenPresent<T> = SqlBuilder.isNotInWhenPresent(values?.asList())
 
-fun <T> isNotInWhenPresent(values: Collection<T?>?): IsNotInWhenPresent<T> = SqlBuilder.isNotInWhenPresent(values)
+fun <T : Any> isNotInWhenPresent(values: Collection<T?>?): IsNotInWhenPresent<T> = SqlBuilder.isNotInWhenPresent(values)
 
-fun <T> isBetween(value1: T & Any): BetweenBuilder<T & Any> = BetweenBuilder(value1)
+fun <T : Any> isBetween(value1: T): BetweenBuilder<T> = BetweenBuilder(value1)
 
-fun <T> isBetweenWhenPresent(value1: T?): BetweenWhenPresentBuilder<T> = BetweenWhenPresentBuilder(value1)
+fun <T : Any> isBetweenWhenPresent(value1: T?): BetweenWhenPresentBuilder<T> = BetweenWhenPresentBuilder(value1)
 
-fun <T> isNotBetween(value1: T & Any): NotBetweenBuilder<T & Any> = NotBetweenBuilder(value1)
+fun <T : Any> isNotBetween(value1: T): NotBetweenBuilder<T> = NotBetweenBuilder(value1)
 
-fun <T> isNotBetweenWhenPresent(value1: T?): NotBetweenWhenPresentBuilder<T> =
+fun <T : Any> isNotBetweenWhenPresent(value1: T?): NotBetweenWhenPresentBuilder<T> =
     NotBetweenWhenPresentBuilder(value1)
 
 // for string columns, but generic for columns with type handlers
-fun <T> isLike(value: T & Any): IsLike<T> = SqlBuilder.isLike(value)
+fun <T : Any> isLike(value: T): IsLike<T> = SqlBuilder.isLike(value)
 
-fun <T> isLikeWhenPresent(value: T?): IsLike<T> = SqlBuilder.isLikeWhenPresent(value)
+fun <T : Any> isLikeWhenPresent(value: T?): IsLikeWhenPresent<T> = SqlBuilder.isLikeWhenPresent(value)
 
-fun <T> isNotLike(value: T & Any): IsNotLike<T> = SqlBuilder.isNotLike(value)
+fun <T : Any> isNotLike(value: T): IsNotLike<T> = SqlBuilder.isNotLike(value)
 
-fun <T> isNotLikeWhenPresent(value: T?): IsNotLike<T> = SqlBuilder.isNotLikeWhenPresent(value)
+fun <T : Any> isNotLikeWhenPresent(value: T?): IsNotLikeWhenPresent<T> = SqlBuilder.isNotLikeWhenPresent(value)
 
 // shortcuts for booleans
 fun isTrue(): IsEqualTo<Boolean> = isEqualTo(true)
@@ -322,50 +338,53 @@ fun isTrue(): IsEqualTo<Boolean> = isEqualTo(true)
 fun isFalse(): IsEqualTo<Boolean> = isEqualTo(false)
 
 // conditions for strings only
-fun isLikeCaseInsensitive(value: String): IsLikeCaseInsensitive = SqlBuilder.isLikeCaseInsensitive(value)
+fun isLikeCaseInsensitive(value: String): IsLikeCaseInsensitive<String> = SqlBuilder.isLikeCaseInsensitive(value)
 
-fun isLikeCaseInsensitiveWhenPresent(value: String?): IsLikeCaseInsensitive =
+fun isLikeCaseInsensitiveWhenPresent(value: String?): IsLikeCaseInsensitiveWhenPresent<String> =
     SqlBuilder.isLikeCaseInsensitiveWhenPresent(value)
 
-fun isNotLikeCaseInsensitive(value: String): IsNotLikeCaseInsensitive = SqlBuilder.isNotLikeCaseInsensitive(value)
+fun isNotLikeCaseInsensitive(value: String): IsNotLikeCaseInsensitive<String> = SqlBuilder.isNotLikeCaseInsensitive(value)
 
-fun isNotLikeCaseInsensitiveWhenPresent(value: String?): IsNotLikeCaseInsensitive =
+fun isNotLikeCaseInsensitiveWhenPresent(value: String?): IsNotLikeCaseInsensitiveWhenPresent<String> =
     SqlBuilder.isNotLikeCaseInsensitiveWhenPresent(value)
 
-fun isInCaseInsensitive(vararg values: String): IsInCaseInsensitive = isInCaseInsensitive(values.asList())
+fun isInCaseInsensitive(vararg values: String): IsInCaseInsensitive<String> = isInCaseInsensitive(values.asList())
 
 @JvmName("isInArrayCaseInsensitive")
-fun isInCaseInsensitive(values: Array<out String>): IsInCaseInsensitive = SqlBuilder.isInCaseInsensitive(values.asList())
+fun isInCaseInsensitive(values: Array<out String>): IsInCaseInsensitive<String> =
+    SqlBuilder.isInCaseInsensitive(values.asList())
 
-fun isInCaseInsensitive(values: Collection<String>): IsInCaseInsensitive = SqlBuilder.isInCaseInsensitive(values)
+fun isInCaseInsensitive(values: Collection<String>): IsInCaseInsensitive<String> =
+    SqlBuilder.isInCaseInsensitive(values)
 
-fun isInCaseInsensitiveWhenPresent(vararg values: String?): IsInCaseInsensitiveWhenPresent =
+fun isInCaseInsensitiveWhenPresent(vararg values: String?): IsInCaseInsensitiveWhenPresent<String> =
     isInCaseInsensitiveWhenPresent(values.asList())
 
 @JvmName("isInArrayCaseInsensitiveWhenPresent")
-fun isInCaseInsensitiveWhenPresent(values: Array<out String?>?): IsInCaseInsensitiveWhenPresent =
+fun isInCaseInsensitiveWhenPresent(values: Array<out String?>?): IsInCaseInsensitiveWhenPresent<String> =
     SqlBuilder.isInCaseInsensitiveWhenPresent(values?.asList())
 
-fun isInCaseInsensitiveWhenPresent(values: Collection<String?>?): IsInCaseInsensitiveWhenPresent =
+fun isInCaseInsensitiveWhenPresent(values: Collection<String?>?): IsInCaseInsensitiveWhenPresent<String> =
     SqlBuilder.isInCaseInsensitiveWhenPresent(values)
 
-fun isNotInCaseInsensitive(vararg values: String): IsNotInCaseInsensitive = isNotInCaseInsensitive(values.asList())
+fun isNotInCaseInsensitive(vararg values: String): IsNotInCaseInsensitive<String> =
+    isNotInCaseInsensitive(values.asList())
 
 @JvmName("isNotInArrayCaseInsensitive")
-fun isNotInCaseInsensitive(values: Array<out String>): IsNotInCaseInsensitive =
+fun isNotInCaseInsensitive(values: Array<out String>): IsNotInCaseInsensitive<String> =
     SqlBuilder.isNotInCaseInsensitive(values.asList())
 
-fun isNotInCaseInsensitive(values: Collection<String>): IsNotInCaseInsensitive =
+fun isNotInCaseInsensitive(values: Collection<String>): IsNotInCaseInsensitive<String> =
     SqlBuilder.isNotInCaseInsensitive(values)
 
-fun isNotInCaseInsensitiveWhenPresent(vararg values: String?): IsNotInCaseInsensitiveWhenPresent =
+fun isNotInCaseInsensitiveWhenPresent(vararg values: String?): IsNotInCaseInsensitiveWhenPresent<String> =
     isNotInCaseInsensitiveWhenPresent(values.asList())
 
 @JvmName("isNotInArrayCaseInsensitiveWhenPresent")
-fun isNotInCaseInsensitiveWhenPresent(values: Array<out String?>?): IsNotInCaseInsensitiveWhenPresent =
+fun isNotInCaseInsensitiveWhenPresent(values: Array<out String?>?): IsNotInCaseInsensitiveWhenPresent<String> =
     SqlBuilder.isNotInCaseInsensitiveWhenPresent(values?.asList())
 
-fun isNotInCaseInsensitiveWhenPresent(values: Collection<String?>?): IsNotInCaseInsensitiveWhenPresent =
+fun isNotInCaseInsensitiveWhenPresent(values: Collection<String?>?): IsNotInCaseInsensitiveWhenPresent<String> =
     SqlBuilder.isNotInCaseInsensitiveWhenPresent(values)
 
 // order by support
@@ -390,22 +409,22 @@ fun sortColumn(name: String): SortSpecification = SqlBuilder.sortColumn(name)
 fun sortColumn(tableAlias: String, column: SqlColumn<*>): SortSpecification = SqlBuilder.sortColumn(tableAlias, column)
 
 // DSL Support Classes
-class BetweenBuilder<T>(private val value1: T) {
+class BetweenBuilder<T : Any>(private val value1: T) {
     fun and(value2: T): IsBetween<T> = SqlBuilder.isBetween(value1).and(value2)
 }
 
-class BetweenWhenPresentBuilder<T>(private val value1: T?) {
-    fun and(value2: T?): IsBetween<T> {
+class BetweenWhenPresentBuilder<T : Any>(private val value1: T?) {
+    fun and(value2: T?): IsBetweenWhenPresent<T> {
         return SqlBuilder.isBetweenWhenPresent<T>(value1).and(value2)
     }
 }
 
-class NotBetweenBuilder<T>(private val value1: T) {
+class NotBetweenBuilder<T : Any>(private val value1: T) {
     fun and(value2: T): IsNotBetween<T> = SqlBuilder.isNotBetween(value1).and(value2)
 }
 
-class NotBetweenWhenPresentBuilder<T>(private val value1: T?) {
-    fun and(value2: T?): IsNotBetween<T> {
+class NotBetweenWhenPresentBuilder<T : Any>(private val value1: T?) {
+    fun and(value2: T?): IsNotBetweenWhenPresent<T> {
         return SqlBuilder.isNotBetweenWhenPresent<T>(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
index a59b74874..bbd79dcd7 100644
--- 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
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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
index 1dd687443..bd25cfab6 100644
--- 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
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 42386e013..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 49a1133fd..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
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 881c3d9d0..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -65,7 +65,7 @@ fun NamedParameterJdbcTemplate.deleteFrom(table: SqlTable, completer: DeleteComp
     delete(org.mybatis.dynamic.sql.util.kotlin.spring.deleteFrom(table, completer))
 
 // batch insert
-fun <T> NamedParameterJdbcTemplate.insertBatch(insertStatement: BatchInsert<T>): IntArray =
+fun <T : Any> NamedParameterJdbcTemplate.insertBatch(insertStatement: BatchInsert<T>): IntArray =
     batchUpdate(insertStatement.insertStatementSQL, BatchInsertUtility.createBatch(insertStatement.records))
 
 fun <T : Any> NamedParameterJdbcTemplate.insertBatch(
@@ -81,10 +81,10 @@ fun <T : Any> NamedParameterJdbcTemplate.insertBatch(
     insertBatch(org.mybatis.dynamic.sql.util.kotlin.spring.insertBatch(records, completer))
 
 // single row insert
-fun <T> NamedParameterJdbcTemplate.insert(insertStatement: InsertStatementProvider<T>): Int =
+fun <T : Any> NamedParameterJdbcTemplate.insert(insertStatement: InsertStatementProvider<T>): Int =
     update(insertStatement.insertStatement, BeanPropertySqlParameterSource(insertStatement))
 
-fun <T> NamedParameterJdbcTemplate.insert(
+fun <T : Any> NamedParameterJdbcTemplate.insert(
     insertStatement: InsertStatementProvider<T>,
     keyHolder: KeyHolder
 ): Int =
@@ -119,10 +119,10 @@ fun <T : Any> NamedParameterJdbcTemplate.insertMultiple(
 ): Int =
     insertMultiple(org.mybatis.dynamic.sql.util.kotlin.spring.insertMultiple(records, completer))
 
-fun <T> NamedParameterJdbcTemplate.insertMultiple(insertStatement: MultiRowInsertStatementProvider<T>): Int =
+fun <T : Any> NamedParameterJdbcTemplate.insertMultiple(insertStatement: MultiRowInsertStatementProvider<T>): Int =
     update(insertStatement.insertStatement, BeanPropertySqlParameterSource(insertStatement))
 
-fun <T> NamedParameterJdbcTemplate.insertMultiple(
+fun <T : Any> NamedParameterJdbcTemplate.insertMultiple(
     insertStatement: MultiRowInsertStatementProvider<T>,
     keyHolder: KeyHolder
 ): Int =
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 30749c0de..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,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties b/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties
index 9927dbeec..3cf560a78 100644
--- a/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties
+++ b/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties
@@ -1,5 +1,5 @@
 #
-#    Copyright 2016-2024 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.
@@ -52,7 +52,7 @@ ERROR.33=Calling "select" or "selectDistinct" more than once is not allowed. Add
   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=You must either implement the "render" or  "renderWithTableAlias" method in a column or function
+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
@@ -60,4 +60,11 @@ 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
 INTERNAL.ERROR=Internal Error {0}
diff --git a/src/site/markdown/docs/caseExpressions.md b/src/site/markdown/docs/caseExpressions.md
index 9b83d4f3e..3f711e626 100644
--- a/src/site/markdown/docs/caseExpressions.md
+++ b/src/site/markdown/docs/caseExpressions.md
@@ -3,9 +3,9 @@
 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 Statements in SQL
+## Case Expressions in SQL
 The library supports different types of case expressions - a "simple" case expression, and a "searched" case
-expressions.
+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:
 
diff --git a/src/site/markdown/docs/conditions.md b/src/site/markdown/docs/conditions.md
index d5683f206..438249400 100644
--- a/src/site/markdown/docs/conditions.md
+++ b/src/site/markdown/docs/conditions.md
@@ -166,8 +166,8 @@ table lists the optional conditions and shows how to use them:
 | Null                      | where(id, isNull().filter(BooleanSupplier)                             | The condition will render if BooleanSupplier.getAsBoolean() returns true                                                                           |
 
 ### "When Present" Condition Builders
-The library supplies several methods that supply 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" condition builder methods.
+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                                               |
 |---------------------------|---------------------------------------------------|---------------------------------------------------------------|
@@ -184,14 +184,20 @@ values. The table below lists the rendering rules for each of these "when presen
 | 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                    |
 
-Note that these methods simply apply a "NotNull" filter to a  condition. For example:
+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
-// the following two lines are functionally equivalent
-... where (id, isEqualToWhenPresent(x)) ...
 ... 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 rules
 are different for the base conditions ("isIn", "isNotIn", etc.) and the "when present" conditions ("isInWhenPresent",
@@ -204,8 +210,8 @@ 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 condition will still render, but the resulting SQL will
-be invalid and will cause a runtime exception. We believe this is the safest outcome. For example, suppose
+"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
@@ -214,12 +220,6 @@ a DELETE statement was coded as follows:
      .and(id, isIn(Collections.emptyList()));
 ```
 
-This statement will be rendered as follows:
-
-```sql
-   delete from foo where status = ? and id in ()
-```
-
 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.
@@ -229,8 +229,8 @@ and the case-insensitive versions of these conditions:
 
 | Input                                    | Effect                                                                            |
 |------------------------------------------|-----------------------------------------------------------------------------------|
-| isIn(null)                               | NullPointerException                                                              |
-| isIn(Collections.emptyList())            | Rendered as "in ()" (Invalid SQL)                                                 |
+| 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                                                            |
@@ -267,7 +267,7 @@ any null or blank string, and you want to trim all strings. This can be accompli
             .where(animalName, isIn("  Mouse", "  ", null, "", "Musk shrew  ")
                     .filter(Objects::nonNull)
                     .map(String::trim)
-                    .filter(st -> !st.isEmpty()))
+                    .filter(not(String::isEmpty)))
             .orderBy(id)
             .build()
             .render(RenderingStrategies.MYBATIS3);
@@ -284,7 +284,7 @@ public class MyInCondition {
         return SqlBuilder.isIn(values)
                .filter(Objects::nonNull)
                .map(String::trim)
-               .filter(st -> !st.isEmpty());
+               .filter(not(String::isEmpty));
     }
 }
 ```
diff --git a/src/site/markdown/docs/extending.md b/src/site/markdown/docs/extending.md
index ae04f434a..a1fee9adb 100644
--- a/src/site/markdown/docs/extending.md
+++ b/src/site/markdown/docs/extending.md
@@ -11,10 +11,10 @@ The SELECT support is the most complex part of the library, and also the part of
 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, using a database function. |
-| `org.mybatis.dynamic.sql.BindableColumn` | Use this interface if you want to add capabilities to a WHERE clause. For example, creating a custom condition.                       |
+| 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
@@ -101,6 +101,7 @@ the function changes the data type from `byte[]` to `String`.
 import java.sql.JDBCType;
 import java.util.Optional;
 
+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;
@@ -108,7 +109,7 @@ import org.mybatis.dynamic.sql.util.FragmentAndParameters;
 
 public class ToBase64 extends AbstractTypeConvertingFunction<byte[], String, ToBase64> {
 
-   protected ToBase64(BindableColumn<byte[]> column) {
+   private ToBase64(BasicColumn column) {
       super(column);
    }
 
@@ -143,13 +144,15 @@ public class ToBase64 extends AbstractTypeConvertingFunction<byte[], String, ToB
 The following function implements the common database `UPPER()` function.
 
 ```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<String, Upper> {
 
-   private Upper(BindableColumn<String> column) {
+   private Upper(BasicColumn column) {
       super(column);
    }
 
@@ -178,19 +181,21 @@ Note that `FragmentAndParameters` has a utility method that can simplify the imp
 add any new parameters to the resulting fragment. For example, the UPPER function can be simplified as follows:
 
 ```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<String, Upper> {
 
-   private Upper(BindableColumn<String> column) {
+   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$
+      return column.render(renderingContext).mapFragment(f -> "upper(" + f + ")"); //$NON-NLS-1$ //$NON-NLS-2$
    }
 
    @Override
@@ -211,9 +216,16 @@ The following function implements the concatenate operator. Note that the operat
 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<T> extends OperatorFunction<T> {
 
-    protected Concatenate(BindableColumn<T> firstColumn, BasicColumn secondColumn,
+    protected Concatenate(BasicColumn firstColumn, BasicColumn secondColumn,
             List<BasicColumn> subsequentColumns) {
         super("||", firstColumn, secondColumn, subsequentColumns); //$NON-NLS-1$
     }
@@ -280,3 +292,89 @@ it.  You can write your own rendering support if you are dissatisfied with the S
 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<T> extends AbstractSingleValueCondition<T>
+        implements AbstractSingleValueCondition.Filterable<T>, AbstractSingleValueCondition.Mappable<T> {
+    private static final IsLikeEscape<?> EMPTY = new IsLikeEscape<Object>(-1, null) {
+        @Override
+        public Object value() {
+            throw new NoSuchElementException("No value present"); //$NON-NLS-1$
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return true;
+        }
+    };
+
+    public static <T> IsLikeEscape<T> empty() {
+        @SuppressWarnings("unchecked")
+        IsLikeEscape<T> t = (IsLikeEscape<T>) 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<T> 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<T> filter(Predicate<? super T> predicate) {
+        return filterSupport(predicate, IsLikeEscape::empty, this);
+    }
+
+    @Override
+    public <R> IsLikeEscape<R> map(Function<? super T, ? extends R> mapper) {
+        return mapSupport(mapper, v -> new IsLikeEscape<>(v, escapeCharacter), IsLikeEscape::empty);
+    }
+
+    public static <T> IsLikeEscape<T> isLike(T value) {
+        return new IsLikeEscape<>(value, null);
+    }
+
+    public static <T> IsLikeEscape<T> isLike(T value, Character escapeCharacter) {
+        return new IsLikeEscape<>(value, escapeCharacter);
+    }
+}
+```
+
+Important notes:
+
+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/kotlinCaseExpressions.md b/src/site/markdown/docs/kotlinCaseExpressions.md
index 5366db40b..d0fdb4b4d 100644
--- a/src/site/markdown/docs/kotlinCaseExpressions.md
+++ b/src/site/markdown/docs/kotlinCaseExpressions.md
@@ -3,9 +3,9 @@
 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 Statements in SQL
+## Case Expressions in SQL
 The library supports different types of case expressions - a "simple" case expression, and a "searched" case
-expressions.
+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:
 
diff --git a/src/site/markdown/docs/kotlinOverview.md b/src/site/markdown/docs/kotlinOverview.md
index 5b646a7b0..78dba6a78 100644
--- a/src/site/markdown/docs/kotlinOverview.md
+++ b/src/site/markdown/docs/kotlinOverview.md
@@ -417,9 +417,9 @@ val selectStatement = select(orderMaster.orderId, orderMaster.orderDate, orderDe
    orderDetail.description, orderDetail.quantity
 ) {
    from(orderMaster, "om")
-   join(orderDetail, "od") {
-      on(orderMaster.orderId) equalTo orderDetail.orderId
-      and(orderMaster.orderId) equalTo orderDetail.orderId
+   join(orderDetail, "od") on {
+      orderMaster.orderId isEqualTo orderDetail.orderId
+      and { orderMaster.orderId isEqualTo orderDetail.orderId }
    }
    where { orderMaster.orderId isEqualTo 1 }
    or {
@@ -433,8 +433,7 @@ val selectStatement = select(orderMaster.orderId, orderMaster.orderDate, orderDe
 
 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. In a join clause, you must
-specify an `on` condition, and you may specify additional `and` conditions as necessary. Full, left, right, inner,
+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.
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/select.md b/src/site/markdown/docs/select.md
index 923c30c42..f73ef17dd 100644
--- a/src/site/markdown/docs/select.md
+++ b/src/site/markdown/docs/select.md
@@ -10,7 +10,7 @@ In general, the following are supported:
 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
+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)`
@@ -21,47 +21,50 @@ At this time, the library does not support the following:
 2. INTERSECT, EXCEPT, etc.
 
 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:
+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<AnimalData> 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<AnimalData> 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:
 
@@ -74,14 +77,14 @@ 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.
@@ -96,16 +99,16 @@ Multi-select queries are a special case of union select statements. The differen
 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)
+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)
+                selectDistinct(id, animalName, bodyWeight, brainWeight)
+                .from(animalData)
+                .orderBy(id.descending())
+                .limit(3)
         )
         .build()
         .render(RenderingStrategies.MYBATIS3);
@@ -114,7 +117,8 @@ paging clauses can be applied to the merged queries. For example:
 ## 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;
@@ -143,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")
@@ -171,7 +177,8 @@ 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:
 
@@ -205,30 +212,33 @@ Order by phrases can be difficult to calculate when there are aliased columns, a
 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 easist to use the "arbitrary string" method with the column alias as shown below.
-1. 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))`.
-1. 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).
+   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 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:
+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:
@@ -237,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
@@ -263,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/springBatch.md b/src/site/markdown/docs/springBatch.md
index 4b1855a97..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<PersonRecord> reader(SqlSessionFactory sqlSessionFactory) {
+    SelectStatementProvider selectStatement =  select(person.allColumns())
+            .from(person)
+            .where(lastName, isEqualTo("flintstone"))
+            .build()
+            .render(RenderingStrategies.MYBATIS3);
+
+    MyBatisCursorItemReader<PersonRecord> 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<PersonRecord> selectMany(Map<String, Object> 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<PersonRecord> 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<PersonRecord> 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<PersonRecord> selectMany(Map<String, Object> 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<Person> 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<Person> 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<Person> selectMany(Map<String, Object> parameterValues);
-```
 
-## Complete Example
+## Complete Examples
 
-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)
+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/resources/css/site.css b/src/site/resources/css/site.css
index 9012c031a..54e023e50 100644
--- a/src/site/resources/css/site.css
+++ b/src/site/resources/css/site.css
@@ -1,5 +1,5 @@
 /**
- *    Copyright 2016-2024 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.
diff --git a/src/site/site.xml b/src/site/site.xml
index e22bf264d..86b32feac 100644
--- a/src/site/site.xml
+++ b/src/site/site.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
 
-       Copyright 2016-2024 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.
@@ -36,6 +36,7 @@
     <menu name="User's Guide">
       <item href="docs/introduction.html" name="Introduction" />
       <item href="docs/CHANGELOG.html" name="Change Log" />
+      <item href="docs/migratingV1toV2.html" name="Migrating from V1 to V2" />
       <item href="docs/quickStart.html" name="Quick Start" />
       <item href="docs/exceptions.html" name="Exceptions thrown by the Library" />
       <item href="docs/configuration.html" name="Configuration of the Library" />
diff --git a/src/test/java/config/TestContainersConfiguration.java b/src/test/java/config/TestContainersConfiguration.java
index ed2c95c96..77fd866b0 100644
--- a/src/test/java/config/TestContainersConfiguration.java
+++ b/src/test/java/config/TestContainersConfiguration.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -21,6 +21,7 @@
  * Utility interface to hold Docker image tags for the test containers we use
  */
 public interface TestContainersConfiguration {
-    DockerImageName POSTGRES_LATEST = DockerImageName.parse("postgres:16.3");
-    DockerImageName MARIADB_LATEST = DockerImageName.parse("mariadb:11.4.2");
+    DockerImageName POSTGRES_LATEST = DockerImageName.parse("postgres:17.2");
+    DockerImageName MARIADB_LATEST = DockerImageName.parse("mariadb:11.6.2");
+    DockerImageName MYSQL_LATEST = DockerImageName.parse("mysql:9.1.0");
 }
diff --git a/src/test/java/examples/animal/data/AnimalData.java b/src/test/java/examples/animal/data/AnimalData.java
index b9755ac30..0c4a49321 100644
--- a/src/test/java/examples/animal/data/AnimalData.java
+++ b/src/test/java/examples/animal/data/AnimalData.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,41 +15,4 @@
  */
 package examples.animal.data;
 
-public class AnimalData {
-    private int id;
-    private String animalName;
-    private double brainWeight;
-    private double bodyWeight;
-
-    public int getId() {
-        return id;
-    }
-
-    public void setId(int id) {
-        this.id = id;
-    }
-
-    public String getAnimalName() {
-        return animalName;
-    }
-
-    public void setAnimalName(String animalName) {
-        this.animalName = animalName;
-    }
-
-    public double getBrainWeight() {
-        return brainWeight;
-    }
-
-    public void setBrainWeight(double brainWeight) {
-        this.brainWeight = brainWeight;
-    }
-
-    public double getBodyWeight() {
-        return bodyWeight;
-    }
-
-    public void setBodyWeight(double bodyWeight) {
-        this.bodyWeight = bodyWeight;
-    }
-}
+public record AnimalData(int id, String animalName, double brainWeight, double bodyWeight) {}
diff --git a/src/test/java/examples/animal/data/AnimalDataDynamicSqlSupport.java b/src/test/java/examples/animal/data/AnimalDataDynamicSqlSupport.java
index 3a2949419..1d8696358 100644
--- a/src/test/java/examples/animal/data/AnimalDataDynamicSqlSupport.java
+++ b/src/test/java/examples/animal/data/AnimalDataDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/animal/data/AnimalDataMapper.java b/src/test/java/examples/animal/data/AnimalDataMapper.java
index 026a55363..26a2cbac2 100644
--- a/src/test/java/examples/animal/data/AnimalDataMapper.java
+++ b/src/test/java/examples/animal/data/AnimalDataMapper.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,10 +17,8 @@
 
 import java.util.List;
 
+import org.apache.ibatis.annotations.Arg;
 import org.apache.ibatis.annotations.Param;
-import org.apache.ibatis.annotations.Result;
-import org.apache.ibatis.annotations.ResultMap;
-import org.apache.ibatis.annotations.Results;
 import org.apache.ibatis.annotations.Select;
 import org.apache.ibatis.annotations.SelectProvider;
 import org.apache.ibatis.session.RowBounds;
@@ -34,20 +32,24 @@
 public interface AnimalDataMapper extends CommonDeleteMapper, CommonInsertMapper<AnimalData>, CommonUpdateMapper {
 
     @SelectProvider(type=SqlProviderAdapter.class, method="select")
-    @Results(id="AnimalDataResult", value={
-        @Result(column="id", property="id", id=true),
-        @Result(column="animal_name", property="animalName"),
-        @Result(column="brain_weight", property="brainWeight"),
-        @Result(column="body_weight", property="bodyWeight")
-    })
+    @Arg(column = "id", javaType = int.class, id = true)
+    @Arg(column = "animal_name", javaType = String.class)
+    @Arg(column = "brain_weight", javaType = double.class)
+    @Arg(column = "body_weight", javaType = double.class)
     List<AnimalData> selectMany(SelectStatementProvider selectStatement);
 
     @SelectProvider(type = SqlProviderAdapter.class, method = "select")
-    @ResultMap("AnimalDataResult")
+    @Arg(column = "id", javaType = int.class, id = true)
+    @Arg(column = "animal_name", javaType = String.class)
+    @Arg(column = "brain_weight", javaType = double.class)
+    @Arg(column = "body_weight", javaType = double.class)
     List<AnimalData> selectManyWithRowBounds(SelectStatementProvider selectStatement, RowBounds rowBounds);
 
     @SelectProvider(type = SqlProviderAdapter.class, method = "select")
-    @ResultMap("AnimalDataResult")
+    @Arg(column = "id", javaType = int.class, id = true)
+    @Arg(column = "animal_name", javaType = String.class)
+    @Arg(column = "brain_weight", javaType = double.class)
+    @Arg(column = "body_weight", javaType = double.class)
     AnimalData selectOne(SelectStatementProvider selectStatement);
 
     @Select({
@@ -55,7 +57,10 @@ public interface AnimalDataMapper extends CommonDeleteMapper, CommonInsertMapper
         "from AnimalData",
         "${whereClause}"
     })
-    @ResultMap("AnimalDataResult")
+    @Arg(column = "id", javaType = int.class, id = true)
+    @Arg(column = "animal_name", javaType = String.class)
+    @Arg(column = "brain_weight", javaType = double.class)
+    @Arg(column = "body_weight", javaType = double.class)
     List<AnimalData> selectWithWhereClause(WhereClauseProvider whereClause);
 
     @Select({
@@ -63,7 +68,10 @@ public interface AnimalDataMapper extends CommonDeleteMapper, CommonInsertMapper
         "from AnimalData a",
         "${whereClause}"
     })
-    @ResultMap("AnimalDataResult")
+    @Arg(column = "id", javaType = int.class, id = true)
+    @Arg(column = "animal_name", javaType = String.class)
+    @Arg(column = "brain_weight", javaType = double.class)
+    @Arg(column = "body_weight", javaType = double.class)
     List<AnimalData> selectWithWhereClauseAndAlias(WhereClauseProvider whereClause);
 
     @Select({
@@ -73,7 +81,10 @@ public interface AnimalDataMapper extends CommonDeleteMapper, CommonInsertMapper
         "order by id",
         "OFFSET #{offset,jdbcType=INTEGER} LIMIT #{limit,jdbcType=INTEGER}"
     })
-    @ResultMap("AnimalDataResult")
+    @Arg(column = "id", javaType = int.class, id = true)
+    @Arg(column = "animal_name", javaType = String.class)
+    @Arg(column = "brain_weight", javaType = double.class)
+    @Arg(column = "body_weight", javaType = double.class)
     List<AnimalData> selectWithWhereClauseLimitAndOffset(@Param("whereClauseProvider") WhereClauseProvider whereClause,
             @Param("limit") int limit, @Param("offset") int offset);
 
@@ -84,7 +95,10 @@ List<AnimalData> selectWithWhereClauseLimitAndOffset(@Param("whereClauseProvider
         "order by id",
         "OFFSET #{offset,jdbcType=INTEGER} LIMIT #{limit,jdbcType=INTEGER}"
     })
-    @ResultMap("AnimalDataResult")
+    @Arg(column = "id", javaType = int.class, id = true)
+    @Arg(column = "animal_name", javaType = String.class)
+    @Arg(column = "brain_weight", javaType = double.class)
+    @Arg(column = "body_weight", javaType = double.class)
     List<AnimalData> selectWithWhereClauseAliasLimitAndOffset(@Param("whereClauseProvider") WhereClauseProvider whereClause,
                                                               @Param("limit") int limit, @Param("offset") int offset);
 }
diff --git a/src/test/java/examples/animal/data/AnimalDataTest.java b/src/test/java/examples/animal/data/AnimalDataTest.java
index d751decab..efedc0aea 100644
--- a/src/test/java/examples/animal/data/AnimalDataTest.java
+++ b/src/test/java/examples/animal/data/AnimalDataTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -33,7 +33,6 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Optional;
 
 import org.apache.ibatis.datasource.unpooled.UnpooledDataSource;
@@ -46,6 +45,7 @@
 import org.apache.ibatis.session.SqlSessionFactory;
 import org.apache.ibatis.session.SqlSessionFactoryBuilder;
 import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mybatis.dynamic.sql.BasicColumn;
@@ -103,7 +103,25 @@ void testSelectAllRows() {
 
             assertAll(
                     () -> assertThat(animals).hasSize(65),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
+            );
+        }
+    }
+
+    @Test
+    void testSelectAllRowsWithNullLimit() {
+        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
+            AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
+            SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
+                    .from(animalData)
+                    .limitWhenPresent(null)
+                    .build()
+                    .render(RenderingStrategies.MYBATIS3);
+            List<AnimalData> animals = mapper.selectMany(selectStatement);
+
+            assertAll(
+                    () -> assertThat(animals).hasSize(65),
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -122,7 +140,7 @@ void testSelectAllRowsWithRowBounds() {
             List<AnimalData> animals = mapper.selectManyWithRowBounds(selectStatement, rowBounds);
             assertAll(
                     () -> assertThat(animals).hasSize(6),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(5)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(5)
             );
         }
     }
@@ -139,7 +157,7 @@ void testSelectAllRowsWithOrder() {
             List<AnimalData> animals = mapper.selectMany(selectStatement);
             assertAll(
                     () -> assertThat(animals).hasSize(65),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(65)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(65)
             );
         }
     }
@@ -178,7 +196,7 @@ void testSelectAllRowsAllColumnsWithOrder() {
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select * from AnimalData order by id DESC"),
                     () -> assertThat(animals).hasSize(65),
                     () -> assertThat(animals).first().isNotNull()
-                            .extracting(AnimalData::getId).isEqualTo(65)
+                            .extracting(AnimalData::id).isEqualTo(65)
             );
         }
     }
@@ -196,7 +214,7 @@ void testSelectAllRowsAllColumnsWithOrderAndAlias() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select ad.* from AnimalData ad order by id DESC"),
                     () -> assertThat(animals).hasSize(65),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(65)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(65)
             );
         }
     }
@@ -362,7 +380,7 @@ void testSelectRowsNotBetweenWithStandaloneWhereClauseLimitAndOffset() {
             assertThat(whereClause).hasValueSatisfying(wc -> {
                 List<AnimalData> animals = mapper.selectWithWhereClauseLimitAndOffset(wc, 5, 15);
                 assertThat(animals).hasSize(5);
-                assertThat(animals.get(0).getId()).isEqualTo(16);
+                assertThat(animals.get(0).id()).isEqualTo(16);
             });
         }
     }
@@ -380,7 +398,7 @@ void testSelectRowsNotBetweenWithStandaloneWhereClauseAliasLimitAndOffset() {
             assertThat(whereClause).hasValueSatisfying(wc -> {
                 List<AnimalData> animals = mapper.selectWithWhereClauseAliasLimitAndOffset(wc, 3, 24);
                 assertThat(animals).hasSize(3);
-                assertThat(animals.get(0).getId()).isEqualTo(25);
+                assertThat(animals.get(0).id()).isEqualTo(25);
             });
 
         }
@@ -692,14 +710,14 @@ void testInConditionWithEventuallyEmptyList() {
 
     @Test
     void testInConditionWithEventuallyEmptyListForceRendering() {
-        List<Integer> inValues = new ArrayList<>();
+        List<@Nullable Integer> inValues = new ArrayList<>();
         inValues.add(null);
         inValues.add(22);
         inValues.add(null);
 
         SelectModel selectModel = select(id, animalName, bodyWeight, brainWeight)
                 .from(animalData)
-                .where(id, isInWhenPresent(inValues).filter(Objects::nonNull).filter(i -> i != 22))
+                .where(id, isInWhenPresent(inValues).filter(i -> i != 22))
                 .build();
 
         assertThatExceptionOfType(NonRenderingWhereClauseException.class).isThrownBy(() ->
@@ -726,7 +744,7 @@ void testInCaseSensitiveCondition() {
 
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(animalName, isInCaseInsensitive("yellow-bellied marmot", "verbet", null))
+                    .where(animalName, isInCaseInsensitiveWhenPresent("yellow-bellied marmot", "verbet", null))
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
 
@@ -774,12 +792,12 @@ void testNotInCaseSensitiveConditionWithNull() {
 
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(animalName, isNotInCaseInsensitive((String)null))
-                    .build()
+                    .where(animalName, isNotInCaseInsensitiveWhenPresent((String) null))
+                    .configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true))                    .build()
                     .render(RenderingStrategies.MYBATIS3);
 
             List<AnimalData> animals = mapper.selectMany(selectStatement);
-            assertThat(animals).isEmpty();
+            assertThat(animals).hasSize(65);
         }
     }
 
@@ -807,7 +825,7 @@ void testNotInConditionWithEventuallyEmptyListForceRendering() {
         SelectModel selectModel = select(id, animalName, bodyWeight, brainWeight)
                 .from(animalData)
                 .where(id, isNotInWhenPresent(null, 22, null)
-                        .filter(Objects::nonNull).filter(i -> i != 22))
+                        .filter(i -> i != 22))
                 .build();
 
         assertThatExceptionOfType(NonRenderingWhereClauseException.class).isThrownBy(() ->
@@ -846,8 +864,8 @@ void testLikeCaseInsensitive() {
 
             assertAll(
                     () -> assertThat(animals).hasSize(2),
-                    () -> assertThat(animals).element(0).isNotNull().extracting(AnimalData::getAnimalName).isEqualTo("Ground squirrel"),
-                    () -> assertThat(animals).element(1).isNotNull().extracting(AnimalData::getAnimalName).isEqualTo("Artic ground squirrel")
+                    () -> assertThat(animals).element(0).isNotNull().extracting(AnimalData::animalName).isEqualTo("Ground squirrel"),
+                    () -> assertThat(animals).element(1).isNotNull().extracting(AnimalData::animalName).isEqualTo("Artic ground squirrel")
             );
         }
     }
@@ -1569,11 +1587,9 @@ void testComplexCondition() {
     void testUpdate() {
         try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
-            AnimalData row = new AnimalData();
-            row.setBodyWeight(2.6);
 
             UpdateStatementProvider updateStatement = update(animalData)
-                    .set(bodyWeight).equalTo(row.getBodyWeight())
+                    .set(bodyWeight).equalTo(2.6)
                     .set(animalName).equalToNull()
                     .where(id, isIn(1, 5, 7))
                     .or(id, isIn(2, 6, 8), and(animalName, isLike("%bat")))
@@ -1627,11 +1643,7 @@ void testUpdateValueOrNullWithNull() {
     void testInsert() {
         try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
-            AnimalData row = new AnimalData();
-            row.setId(100);
-            row.setAnimalName("Old Shep");
-            row.setBodyWeight(22.5);
-            row.setBrainWeight(1.2);
+            AnimalData row = new AnimalData(100, "Old Shep", 22.5, 1.2);
 
             InsertStatementProvider<AnimalData> insertStatement = insert(row)
                     .into(animalData)
@@ -1651,11 +1663,7 @@ void testInsert() {
     void testInsertNull() {
         try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
-            AnimalData row = new AnimalData();
-            row.setId(100);
-            row.setAnimalName("Old Shep");
-            row.setBodyWeight(22.5);
-            row.setBrainWeight(1.2);
+            AnimalData row = new AnimalData(100, "Old Shep", 22.5, 1.2);
 
             InsertStatementProvider<AnimalData> insertStatement = insert(row)
                     .into(animalData)
@@ -1675,18 +1683,10 @@ void testInsertNull() {
     void testBulkInsert() {
         try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
-            List<AnimalData> records = new ArrayList<>();
-            AnimalData row = new AnimalData();
-            row.setId(100);
-            row.setAnimalName("Old Shep");
-            row.setBodyWeight(22.5);
-            records.add(row);
-
-            row = new AnimalData();
-            row.setId(101);
-            row.setAnimalName("Old Dan");
-            row.setBodyWeight(22.5);
-            records.add(row);
+            List<AnimalData> records = List.of(
+                    new AnimalData(100, "Old Shep", 0.0, 22.5),
+                    new AnimalData(101, "Old Dan", 0.0, 22.5)
+            );
 
             BatchInsert<AnimalData> batchInsert = insertBatch(records)
                     .into(animalData)
@@ -1712,10 +1712,10 @@ void testBulkInsert() {
             assertAll(
                     () -> assertThat(animals).hasSize(2),
                     () -> assertThat(animals).element(0).isNotNull()
-                            .extracting(AnimalData::getId, AnimalData::getBrainWeight, AnimalData::getAnimalName)
+                            .extracting(AnimalData::id, AnimalData::brainWeight, AnimalData::animalName)
                             .containsExactly(100, 1.2, null),
                     () -> assertThat(animals).element(1).isNotNull()
-                            .extracting(AnimalData::getId, AnimalData::getBrainWeight, AnimalData::getAnimalName)
+                            .extracting(AnimalData::id, AnimalData::brainWeight, AnimalData::animalName)
                             .containsExactly(101, 1.2, null)
             );
         }
@@ -1725,18 +1725,10 @@ void testBulkInsert() {
     void testBulkInsert2() {
         try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
-            List<AnimalData> records = new ArrayList<>();
-            AnimalData row = new AnimalData();
-            row.setId(100);
-            row.setAnimalName("Old Shep");
-            row.setBodyWeight(22.5);
-            records.add(row);
-
-            row = new AnimalData();
-            row.setId(101);
-            row.setAnimalName("Old Dan");
-            row.setBodyWeight(22.5);
-            records.add(row);
+            List<AnimalData> records = List.of(
+                    new AnimalData(100, "Old Shep", 0.0, 22.5),
+                    new AnimalData(101, "Old Dan", 0.0, 22.5)
+            );
 
             BatchInsert<AnimalData> batchInsert = insertBatch(records)
                     .into(animalData)
@@ -1762,10 +1754,10 @@ void testBulkInsert2() {
             assertAll(
                     () -> assertThat(animals).hasSize(2),
                     () -> assertThat(animals).element(0).isNotNull()
-                            .extracting(AnimalData::getId, AnimalData::getBrainWeight, AnimalData::getAnimalName)
+                            .extracting(AnimalData::id, AnimalData::brainWeight, AnimalData::animalName)
                             .containsExactly(100, 1.2, "Old Fred"),
                     () -> assertThat(animals).element(1).isNotNull()
-                            .extracting(AnimalData::getId, AnimalData::getBrainWeight, AnimalData::getAnimalName)
+                            .extracting(AnimalData::id, AnimalData::brainWeight, AnimalData::animalName)
                             .containsExactly(101, 1.2, "Old Fred")
             );
         }
@@ -1788,7 +1780,7 @@ void testOrderByAndDistinct() {
 
             assertAll(
                     () -> assertThat(rows).hasSize(14),
-                    () -> assertThat(rows).first().isNotNull().extracting(AnimalData::getId).isEqualTo(65)
+                    () -> assertThat(rows).first().isNotNull().extracting(AnimalData::id).isEqualTo(65)
             );
         }
     }
@@ -1810,7 +1802,7 @@ void testOrderByWithFullClause() {
 
             assertAll(
                     () -> assertThat(rows).hasSize(14),
-                    () -> assertThat(rows).first().isNotNull().extracting(AnimalData::getId).isEqualTo(65)
+                    () -> assertThat(rows).first().isNotNull().extracting(AnimalData::id).isEqualTo(65)
             );
         }
     }
@@ -1926,7 +1918,7 @@ void testMaxSubselect() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select a.id, a.animal_name, a.body_weight, a.brain_weight from AnimalData a where a.brain_weight = (select max(b.brain_weight) from AnimalData b)"),
                     () -> assertThat(records).hasSize(1),
-                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::getAnimalName).isEqualTo("Brachiosaurus")
+                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::animalName).isEqualTo("Brachiosaurus")
             );
         }
     }
@@ -1986,7 +1978,7 @@ void testMinSubselect() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select a.id, a.animal_name, a.body_weight, a.brain_weight from AnimalData a where a.brain_weight <> (select min(b.brain_weight) from AnimalData b) order by animal_name"),
                     () -> assertThat(records).hasSize(64),
-                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::getAnimalName).isEqualTo("African elephant")
+                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::animalName).isEqualTo("African elephant")
             );
         }
     }
@@ -2008,7 +2000,7 @@ void testMinSubselectNoAlias() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where brain_weight <> (select min(brain_weight) from AnimalData) order by animal_name"),
                     () -> assertThat(records).hasSize(64),
-                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::getAnimalName).isEqualTo("African elephant")
+                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::animalName).isEqualTo("African elephant")
             );
         }
     }
@@ -2317,13 +2309,13 @@ void testUpdateWithAddAndSubtract() {
             assertThat(rows).isEqualTo(1);
 
             AnimalData row = MyBatis3Utils.selectOne(mapper::selectOne,
-                    BasicColumn.columnList(id, bodyWeight, brainWeight),
+                    BasicColumn.columnList(id, animalName, bodyWeight, brainWeight),
                     animalData,
                     c -> c.where(id, isEqualTo(1))
             );
 
-            assertThat(row.getBodyWeight()).isEqualTo(-2.86);
-            assertThat(row.getBrainWeight()).isEqualTo(2.005);
+            assertThat(row.bodyWeight()).isEqualTo(-2.86);
+            assertThat(row.brainWeight()).isEqualTo(2.005);
         }
     }
 
@@ -2350,13 +2342,13 @@ void testUpdateWithMultiplyAndDivide() {
             assertThat(rows).isEqualTo(1);
 
             AnimalData row = MyBatis3Utils.selectOne(mapper::selectOne,
-                    BasicColumn.columnList(id, bodyWeight, brainWeight),
+                    BasicColumn.columnList(id, animalName, bodyWeight, brainWeight),
                     animalData,
                     c -> c.where(id, isEqualTo(1))
             );
 
-            assertThat(row.getBodyWeight()).isEqualTo(0.42, within(.001));
-            assertThat(row.getBrainWeight()).isEqualTo(.0025);
+            assertThat(row.bodyWeight()).isEqualTo(0.42, within(.001));
+            assertThat(row.brainWeight()).isEqualTo(.0025);
         }
     }
 }
diff --git a/src/test/java/examples/animal/data/BindingTest.java b/src/test/java/examples/animal/data/BindingTest.java
index 44952965d..f71a6eb58 100644
--- a/src/test/java/examples/animal/data/BindingTest.java
+++ b/src/test/java/examples/animal/data/BindingTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -51,6 +51,7 @@ class BindingTest {
     void setup() throws Exception {
         Class.forName(JDBC_DRIVER);
         InputStream is = getClass().getResourceAsStream("/examples/animal/data/CreateAnimalData.sql");
+        assert is != null;
         try (Connection connection = DriverManager.getConnection(JDBC_URL, "sa", "")) {
             ScriptRunner sr = new ScriptRunner(connection);
             sr.setLogWriter(null);
@@ -66,8 +67,7 @@ void setup() throws Exception {
 
     @Test
     void testBindInSelectList() {
-        SqlSession sqlSession = sqlSessionFactory.openSession();
-        try {
+        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
             Connection connection = sqlSession.getConnection();
 
             PreparedStatement ps = connection.prepareStatement("select brain_weight + ? as calc from AnimalData where id = ?");
@@ -86,15 +86,12 @@ void testBindInSelectList() {
             assertThat(calculatedWeight).isEqualTo(1.005);
         } catch (SQLException e) {
             fail("SQL Exception", e);
-        } finally {
-            sqlSession.close();
         }
     }
 
     @Test
     void testBindInWeirdWhere() {
-        SqlSession sqlSession = sqlSessionFactory.openSession();
-        try {
+        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
             Connection connection = sqlSession.getConnection();
 
             PreparedStatement ps = connection.prepareStatement("select brain_weight from AnimalData where brain_weight + ? > ? and id = ?");
@@ -114,8 +111,6 @@ void testBindInWeirdWhere() {
             assertThat(calculatedWeight).isEqualTo(.005);
         } catch (SQLException e) {
             fail("SQL Exception", e);
-        } finally {
-            sqlSession.close();
         }
     }
 }
diff --git a/src/test/java/examples/animal/data/CaseExpressionTest.java b/src/test/java/examples/animal/data/CaseExpressionTest.java
index 322d9504b..84a293a46 100644
--- a/src/test/java/examples/animal/data/CaseExpressionTest.java
+++ b/src/test/java/examples/animal/data/CaseExpressionTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/animal/data/CommonSelectMapperTest.java b/src/test/java/examples/animal/data/CommonSelectMapperTest.java
index cd20d88fa..f201d963f 100644
--- a/src/test/java/examples/animal/data/CommonSelectMapperTest.java
+++ b/src/test/java/examples/animal/data/CommonSelectMapperTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -69,14 +69,11 @@ void setup() throws Exception {
         sqlSessionFactory = new SqlSessionFactoryBuilder().build(config);
     }
 
-    private final Function<Map<String, Object>, AnimalData> rowMapper = map -> {
-        AnimalData ad = new AnimalData();
-        ad.setId((Integer) map.get("ID"));
-        ad.setAnimalName((String) map.get("ANIMAL_NAME"));
-        ad.setBodyWeight((Double) map.get("BODY_WEIGHT"));
-        ad.setBrainWeight((Double) map.get("BRAIN_WEIGHT"));
-        return ad;
-    };
+    private final Function<Map<String, Object>, AnimalData> rowMapper = map -> new AnimalData(
+            (Integer) map.get("ID"),
+            (String) map.get("ANIMAL_NAME"),
+            (Double) map.get("BRAIN_WEIGHT"),
+            (Double) map.get("BODY_WEIGHT"));
 
     @Test
     void testGeneralSelectOne() {
@@ -106,10 +103,26 @@ void testGeneralSelectOneWithRowMapper() {
 
             AnimalData animal = mapper.selectOne(selectStatement, rowMapper);
 
-            assertThat(animal.getId()).isEqualTo(1);
-            assertThat(animal.getAnimalName()).isEqualTo("Lesser short-tailed shrew");
-            assertThat(animal.getBodyWeight()).isEqualTo(0.14);
-            assertThat(animal.getBrainWeight()).isEqualTo(0.005);
+            assertThat(animal).isNotNull();
+            assertThat(animal.id()).isEqualTo(1);
+            assertThat(animal.animalName()).isEqualTo("Lesser short-tailed shrew");
+            assertThat(animal.bodyWeight()).isEqualTo(0.14);
+            assertThat(animal.brainWeight()).isEqualTo(0.005);
+        }
+    }
+
+    @Test
+    void testGeneralSelectOneWithRowMapperAndNullRow() {
+        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
+            CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class);
+            SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
+                    .from(animalData)
+                    .where(id, isEqualTo(-237))
+                    .build()
+                    .render(RenderingStrategies.MYBATIS3);
+
+            AnimalData animal = mapper.selectOne(selectStatement, rowMapper);
+            assertThat(animal).isNull();
         }
     }
 
@@ -148,14 +161,14 @@ void testGeneralSelectManyWithRowMapper() {
 
             assertThat(rows).hasSize(2);
 
-            assertThat(rows.get(0).getId()).isEqualTo(1);
-            assertThat(rows.get(0).getAnimalName()).isEqualTo("Lesser short-tailed shrew");
-            assertThat(rows.get(0).getBodyWeight()).isEqualTo(0.14);
-            assertThat(rows.get(0).getBrainWeight()).isEqualTo(0.005);
-            assertThat(rows.get(1).getId()).isEqualTo(2);
-            assertThat(rows.get(1).getAnimalName()).isEqualTo("Little brown bat");
-            assertThat(rows.get(1).getBodyWeight()).isEqualTo(0.25);
-            assertThat(rows.get(1).getBrainWeight()).isEqualTo(0.01);
+            assertThat(rows.get(0).id()).isEqualTo(1);
+            assertThat(rows.get(0).animalName()).isEqualTo("Lesser short-tailed shrew");
+            assertThat(rows.get(0).bodyWeight()).isEqualTo(0.14);
+            assertThat(rows.get(0).brainWeight()).isEqualTo(0.005);
+            assertThat(rows.get(1).id()).isEqualTo(2);
+            assertThat(rows.get(1).animalName()).isEqualTo("Little brown bat");
+            assertThat(rows.get(1).bodyWeight()).isEqualTo(0.25);
+            assertThat(rows.get(1).brainWeight()).isEqualTo(0.01);
         }
     }
 
diff --git a/src/test/java/examples/animal/data/FetchFirstTest.java b/src/test/java/examples/animal/data/FetchFirstTest.java
index 4466f152f..9a1a10c00 100644
--- a/src/test/java/examples/animal/data/FetchFirstTest.java
+++ b/src/test/java/examples/animal/data/FetchFirstTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -80,7 +80,7 @@ void testOffsetAndFetchFirstAfterFrom() {
 
             assertAll(
                     () -> assertThat(records).hasSize(3),
-                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::getId).isEqualTo(23),
+                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::id).isEqualTo(23),
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select * from AnimalData offset #{parameters.p1} rows fetch first #{parameters.p2} rows only"),
                     () -> assertThat(selectStatement.getParameters()).containsEntry("p2", 3L),
                     () -> assertThat(selectStatement.getParameters()).containsEntry("p1", 22L)
@@ -102,7 +102,7 @@ void testFetchFirstOnlyAfterFrom() {
 
             assertAll(
                     () -> assertThat(records).hasSize(3),
-                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1),
+                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::id).isEqualTo(1),
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select * from AnimalData fetch first #{parameters.p1} rows only"),
                     () -> assertThat(selectStatement.getParameters()).containsEntry("p1", 3L)
             );
@@ -126,7 +126,7 @@ void testOffsetAndFetchFirstAfterWhere() {
 
             assertAll(
                     () -> assertThat(records).hasSize(3),
-                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::getId).isEqualTo(45),
+                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::id).isEqualTo(45),
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select * from AnimalData where id < #{parameters.p1,jdbcType=INTEGER} and id > #{parameters.p2,jdbcType=INTEGER} offset #{parameters.p3} rows fetch first #{parameters.p4} rows only"),
                     () -> assertThat(selectStatement.getParameters()).contains(entry("p4", 3L), entry("p3", 22L))
             );
@@ -148,7 +148,7 @@ void testFetchFirstOnlyAfterWhere() {
 
             assertAll(
                     () -> assertThat(records).hasSize(3),
-                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1),
+                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::id).isEqualTo(1),
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select * from AnimalData where id < #{parameters.p1,jdbcType=INTEGER} fetch first #{parameters.p2} rows only"),
                     () -> assertThat(selectStatement.getParameters()).containsEntry("p2", 3L)
             );
@@ -171,7 +171,7 @@ void testOffsetAndFetchFirstAfterOrderBy() {
 
             assertAll(
                     () -> assertThat(records).hasSize(3),
-                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::getId).isEqualTo(23),
+                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::id).isEqualTo(23),
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select * from AnimalData order by id offset #{parameters.p1} rows fetch first #{parameters.p2} rows only"),
                     () -> assertThat(selectStatement)
                             .extracting(SelectStatementProvider::getParameters)
@@ -196,7 +196,7 @@ void testLimitOnlyAfterOrderBy() {
 
             assertAll(
                     () -> assertThat(records).hasSize(3),
-                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1),
+                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::id).isEqualTo(1),
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select * from AnimalData order by id fetch first #{parameters.p1} rows only"),
                     () -> assertThat(selectStatement.getParameters()).containsEntry("p1", 3L)
             );
diff --git a/src/test/java/examples/animal/data/Length.java b/src/test/java/examples/animal/data/Length.java
index cf779c15e..a7a8abcb5 100644
--- a/src/test/java/examples/animal/data/Length.java
+++ b/src/test/java/examples/animal/data/Length.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,13 +18,14 @@
 import java.sql.JDBCType;
 import java.util.Optional;
 
+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 Length extends AbstractTypeConvertingFunction<Object, Integer, Length> {
-    private Length(BindableColumn<Object> column) {
+    private Length(BasicColumn column) {
         super(column);
     }
 
@@ -35,11 +36,8 @@ public Optional<JDBCType> jdbcType() {
 
     @Override
     public FragmentAndParameters render(RenderingContext renderingContext) {
-        FragmentAndParameters renderedColumn = column.render(renderingContext);
-
-        return FragmentAndParameters.withFragment("length(" + renderedColumn.fragment() + ")") //$NON-NLS-1$ //$NON-NLS-2$
-                .withParameters(renderedColumn.parameters())
-                .build();
+        return column.render(renderingContext)
+                .mapFragment(f -> "length(" + f + ")"); //$NON-NLS-1$ //$NON-NLS-2$
     }
 
     @Override
@@ -48,8 +46,6 @@ protected Length copy() {
     }
 
     public static Length length(BindableColumn<?> column) {
-        @SuppressWarnings("unchecked")
-        BindableColumn<Object> c = (BindableColumn<Object>) column;
-        return new Length(c);
+        return new Length(column);
     }
 }
diff --git a/src/test/java/examples/animal/data/LimitAndOffsetTest.java b/src/test/java/examples/animal/data/LimitAndOffsetTest.java
index 5267adde3..9569d0751 100644
--- a/src/test/java/examples/animal/data/LimitAndOffsetTest.java
+++ b/src/test/java/examples/animal/data/LimitAndOffsetTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -79,7 +79,7 @@ void testLimitAndOffsetAfterFrom() {
 
             assertAll(
                     () -> assertThat(records).hasSize(3),
-                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::getId).isEqualTo(23),
+                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::id).isEqualTo(23),
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select * from AnimalData limit #{parameters.p1} offset #{parameters.p2}"),
                     () -> assertThat(selectStatement.getParameters()).containsEntry("p1", 3L),
                     () -> assertThat(selectStatement.getParameters()).containsEntry("p2", 22L)
@@ -101,7 +101,7 @@ void testLimitOnlyAfterFrom() {
 
             assertAll(
                     () -> assertThat(records).hasSize(3),
-                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1),
+                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::id).isEqualTo(1),
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select * from AnimalData limit #{parameters.p1}"),
                     () -> assertThat(selectStatement.getParameters()).containsEntry("p1", 3L)
             );
@@ -122,7 +122,7 @@ void testOffsetOnlyAfterFrom() {
 
             assertAll(
                     () -> assertThat(records).hasSize(43),
-                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::getId).isEqualTo(23),
+                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::id).isEqualTo(23),
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select * from AnimalData offset #{parameters.p1} rows"),
                     () -> assertThat(selectStatement.getParameters()).containsEntry("p1", 22L)
             );
@@ -146,7 +146,7 @@ void testLimitAndOffsetAfterWhere() {
 
             assertAll(
                     () -> assertThat(records).hasSize(3),
-                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::getId).isEqualTo(45),
+                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::id).isEqualTo(45),
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select * from AnimalData where id < #{parameters.p1,jdbcType=INTEGER} and id > #{parameters.p2,jdbcType=INTEGER} limit #{parameters.p3} offset #{parameters.p4}"),
                     () -> assertThat(selectStatement.getParameters()).containsEntry("p3", 3L),
                     () -> assertThat(selectStatement.getParameters()).containsEntry("p4", 22L)
@@ -169,7 +169,7 @@ void testLimitOnlyAfterWhere() {
 
             assertAll(
                     () -> assertThat(records).hasSize(3),
-                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1),
+                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::id).isEqualTo(1),
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select * from AnimalData where id < #{parameters.p1,jdbcType=INTEGER} limit #{parameters.p2}"),
                     () -> assertThat(selectStatement.getParameters()).containsEntry("p2", 3L)
             );
@@ -191,7 +191,7 @@ void testOffsetOnlyAfterWhere() {
 
             assertAll(
                     () -> assertThat(records).hasSize(27),
-                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::getId).isEqualTo(23),
+                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::id).isEqualTo(23),
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select * from AnimalData where id < #{parameters.p1,jdbcType=INTEGER} offset #{parameters.p2} rows"),
                     () -> assertThat(selectStatement.getParameters()).containsEntry("p2", 22L)
             );
@@ -214,7 +214,7 @@ void testLimitAndOffsetAfterOrderBy() {
 
             assertAll(
                     () -> assertThat(records).hasSize(3),
-                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::getId).isEqualTo(23),
+                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::id).isEqualTo(23),
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select * from AnimalData order by id limit #{parameters.p1} offset #{parameters.p2}"),
                     () -> assertThat(selectStatement.getParameters()).containsEntry("p1", 3L),
                     () -> assertThat(selectStatement.getParameters()).containsEntry("p2", 22L)
@@ -237,7 +237,7 @@ void testLimitOnlyAfterOrderBy() {
 
             assertAll(
                     () -> assertThat(records).hasSize(3),
-                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1),
+                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::id).isEqualTo(1),
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select * from AnimalData order by id limit #{parameters.p1}"),
                     () -> assertThat(selectStatement.getParameters()).containsEntry("p1", 3L)
             );
@@ -259,7 +259,7 @@ void testOffsetOnlyAfterOrderBy() {
 
             assertAll(
                     () -> assertThat(records).hasSize(43),
-                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::getId).isEqualTo(23),
+                    () -> assertThat(records).first().isNotNull().extracting(AnimalData::id).isEqualTo(23),
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select * from AnimalData order by id offset #{parameters.p1} rows"),
                     () -> assertThat(selectStatement.getParameters()).containsEntry("p1", 22L)
             );
diff --git a/src/test/java/examples/animal/data/MyInCondition.java b/src/test/java/examples/animal/data/MyInCondition.java
index 86035a838..13f076331 100644
--- a/src/test/java/examples/animal/data/MyInCondition.java
+++ b/src/test/java/examples/animal/data/MyInCondition.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,16 +15,16 @@
  */
 package examples.animal.data;
 
-import java.util.Objects;
+import static java.util.function.Predicate.not;
 
+import org.jspecify.annotations.Nullable;
 import org.mybatis.dynamic.sql.SqlBuilder;
-import org.mybatis.dynamic.sql.where.condition.IsIn;
+import org.mybatis.dynamic.sql.where.condition.IsInWhenPresent;
 
 public class MyInCondition {
-    public static IsIn<String> isIn(String...values) {
-        return SqlBuilder.isIn(values)
-                .filter(Objects::nonNull)
-                .map((String::trim))
-                .filter(st -> !st.isEmpty());
+    public static IsInWhenPresent<String> isIn(@Nullable String...values) {
+        return SqlBuilder.isInWhenPresent(values)
+                .map(String::trim)
+                .filter(not(String::isEmpty));
     }
 }
diff --git a/src/test/java/examples/animal/data/OptionalConditionsAnimalDataTest.java b/src/test/java/examples/animal/data/OptionalConditionsAnimalDataTest.java
index acf2cd2a3..0917160a0 100644
--- a/src/test/java/examples/animal/data/OptionalConditionsAnimalDataTest.java
+++ b/src/test/java/examples/animal/data/OptionalConditionsAnimalDataTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -34,6 +34,7 @@
 import org.apache.ibatis.session.SqlSessionFactory;
 import org.apache.ibatis.session.SqlSessionFactoryBuilder;
 import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mybatis.dynamic.sql.render.RenderingStrategies;
@@ -43,7 +44,7 @@ class OptionalConditionsAnimalDataTest {
 
     private static final String JDBC_URL = "jdbc:hsqldb:mem:aname";
     private static final String JDBC_DRIVER = "org.hsqldb.jdbcDriver";
-    private static final Integer NULL_INTEGER = null;
+    private static final @Nullable Integer NULL_INTEGER = null;
 
     private SqlSessionFactory sqlSessionFactory;
 
@@ -80,8 +81,8 @@ void testAllIgnored() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData order by id"),
                     () -> assertThat(animals).hasSize(65),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1),
-                    () -> assertThat(animals).element(1).isNotNull().extracting(AnimalData::getId).isEqualTo(2)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1),
+                    () -> assertThat(animals).element(1).isNotNull().extracting(AnimalData::id).isEqualTo(2)
             );
         }
     }
@@ -102,8 +103,8 @@ void testIgnoredBetweenRendered() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id = #{parameters.p1,jdbcType=INTEGER} or id = #{parameters.p2,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(2),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(3),
-                    () -> assertThat(animals).element(1).isNotNull().extracting(AnimalData::getId).isEqualTo(4)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(3),
+                    () -> assertThat(animals).element(1).isNotNull().extracting(AnimalData::id).isEqualTo(4)
             );
         }
     }
@@ -124,8 +125,8 @@ void testIgnoredInWhere() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id = #{parameters.p1,jdbcType=INTEGER} or id = #{parameters.p2,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(2),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(3),
-                    () -> assertThat(animals).element(1).isNotNull().extracting(AnimalData::getId).isEqualTo(4)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(3),
+                    () -> assertThat(animals).element(1).isNotNull().extracting(AnimalData::id).isEqualTo(4)
             );
         }
     }
@@ -147,8 +148,8 @@ void testManyIgnored() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id = #{parameters.p1,jdbcType=INTEGER} or id = #{parameters.p2,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(2),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(3),
-                    () -> assertThat(animals).element(1).isNotNull().extracting(AnimalData::getId).isEqualTo(4)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(3),
+                    () -> assertThat(animals).element(1).isNotNull().extracting(AnimalData::id).isEqualTo(4)
             );
         }
     }
@@ -168,8 +169,8 @@ void testIgnoredInitialWhere() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id = #{parameters.p1,jdbcType=INTEGER} or id = #{parameters.p2,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(2),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(3),
-                    () -> assertThat(animals).element(1).isNotNull().extracting(AnimalData::getId).isEqualTo(4)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(3),
+                    () -> assertThat(animals).element(1).isNotNull().extracting(AnimalData::id).isEqualTo(4)
             );
         }
     }
@@ -188,7 +189,7 @@ void testEqualWhenPresentWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id = #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(1),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(4)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(4)
             );
         }
     }
@@ -208,7 +209,7 @@ void testEqualWhenPresentWithoutValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -228,7 +229,7 @@ void testNotEqualWhenPresentWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <> #{parameters.p1,jdbcType=INTEGER} and id <= #{parameters.p2,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(9),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -248,7 +249,7 @@ void testNotEqualWhenPresentWithoutValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -268,7 +269,7 @@ void testGreaterThanWhenPresentWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id > #{parameters.p1,jdbcType=INTEGER} and id <= #{parameters.p2,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(6),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(5)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(5)
             );
         }
     }
@@ -288,7 +289,7 @@ void testGreaterThanWhenPresentWithoutValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -308,7 +309,7 @@ void testGreaterThanOrEqualToWhenPresentWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id >= #{parameters.p1,jdbcType=INTEGER} and id <= #{parameters.p2,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(7),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(4)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(4)
             );
         }
     }
@@ -328,7 +329,7 @@ void testGreaterThanOrEqualToWhenPresentWithoutValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -347,7 +348,7 @@ void testLessThanWhenPresentWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id < #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(3),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -367,7 +368,7 @@ void testLessThanWhenPresentWithoutValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -386,7 +387,7 @@ void testLessThanOrEqualToWhenPresentWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(4),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -406,7 +407,7 @@ void testLessThanOrEqualToWhenPresentWithoutValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -425,7 +426,7 @@ void testIsInWhenPresentWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id in (#{parameters.p1,jdbcType=INTEGER},#{parameters.p2,jdbcType=INTEGER},#{parameters.p3,jdbcType=INTEGER}) order by id"),
                     () -> assertThat(animals).hasSize(3),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(4)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(4)
             );
         }
     }
@@ -444,7 +445,7 @@ void testIsInWhenPresentWithSomeValues() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id in (#{parameters.p1,jdbcType=INTEGER},#{parameters.p2,jdbcType=INTEGER}) order by id"),
                     () -> assertThat(animals).hasSize(2),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(3)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(3)
             );
         }
     }
@@ -464,7 +465,7 @@ void testIsInWhenPresentWithNoValues() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -483,7 +484,7 @@ void testIsInCaseInsensitiveWhenPresentWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where upper(animal_name) in (#{parameters.p1,jdbcType=VARCHAR},#{parameters.p2,jdbcType=VARCHAR}) order by id"),
                     () -> assertThat(animals).hasSize(2),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(4)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(4)
             );
         }
     }
@@ -502,7 +503,7 @@ void testIsInCaseInsensitiveWhenPresentWithSomeValues() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where upper(animal_name) in (#{parameters.p1,jdbcType=VARCHAR},#{parameters.p2,jdbcType=VARCHAR}) order by id"),
                     () -> assertThat(animals).hasSize(2),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(4)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(4)
             );
         }
     }
@@ -522,7 +523,7 @@ void testIsInCaseInsensitiveWhenPresentWithNoValues() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -542,7 +543,7 @@ void testIsNotInWhenPresentWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id not in (#{parameters.p1,jdbcType=INTEGER},#{parameters.p2,jdbcType=INTEGER},#{parameters.p3,jdbcType=INTEGER}) and id <= #{parameters.p4,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(7),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -562,7 +563,7 @@ void testIsNotInWhenPresentWithSomeValues() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id not in (#{parameters.p1,jdbcType=INTEGER},#{parameters.p2,jdbcType=INTEGER}) and id <= #{parameters.p3,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(8),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -582,7 +583,7 @@ void testIsNotInWhenPresentWithNoValues() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -602,7 +603,7 @@ void testIsNotInCaseInsensitiveWhenPresentWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where upper(animal_name) not in (#{parameters.p1,jdbcType=VARCHAR},#{parameters.p2,jdbcType=VARCHAR}) and id <= #{parameters.p3,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(8),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -622,7 +623,7 @@ void testIsNotInCaseInsensitiveWhenPresentWithSomeValues() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where upper(animal_name) not in (#{parameters.p1,jdbcType=VARCHAR},#{parameters.p2,jdbcType=VARCHAR}) and id <= #{parameters.p3,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(8),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -642,7 +643,7 @@ void testIsNotInCaseInsensitiveWhenPresentWithNoValues() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -661,7 +662,7 @@ void testIsBetweenWhenPresentWithValues() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id between #{parameters.p1,jdbcType=INTEGER} and #{parameters.p2,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(4),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(3)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(3)
             );
         }
     }
@@ -681,7 +682,7 @@ void testIsBetweenWhenPresentWithFirstMissing() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -701,7 +702,7 @@ void testIsBetweenWhenPresentWithSecondMissing() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -721,7 +722,7 @@ void testIsBetweenWhenPresentWithBothMissing() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -741,7 +742,7 @@ void testIsNotBetweenWhenPresentWithValues() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id not between #{parameters.p1,jdbcType=INTEGER} and #{parameters.p2,jdbcType=INTEGER} and id <= #{parameters.p3,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(6),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -761,7 +762,7 @@ void testIsNotBetweenWhenPresentWithFirstMissing() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -781,7 +782,7 @@ void testIsNotBetweenWhenPresentWithSecondMissing() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -801,7 +802,7 @@ void testIsNotBetweenWhenPresentWithBothMissing() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -821,7 +822,7 @@ void testIsLikeWhenPresentWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where animal_name like #{parameters.p1,jdbcType=VARCHAR} and id <= #{parameters.p2,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(2),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(6)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(6)
             );
         }
     }
@@ -841,7 +842,7 @@ void testIsLikeWhenPresentWithoutValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -861,7 +862,7 @@ void testIsLikeCaseInsensitiveWhenPresentWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where upper(animal_name) like #{parameters.p1,jdbcType=VARCHAR} and id <= #{parameters.p2,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(2),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(6)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(6)
             );
         }
     }
@@ -881,7 +882,7 @@ void testIsLikeCaseInsensitiveWhenPresentWithoutValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -901,7 +902,7 @@ void testIsNotLikeWhenPresentWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where animal_name not like #{parameters.p1,jdbcType=VARCHAR} and id <= #{parameters.p2,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(8),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -921,7 +922,7 @@ void testIsNotLikeWhenPresentWithoutValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -941,7 +942,7 @@ void testIsNotLikeCaseInsensitiveWhenPresentWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where upper(animal_name) not like #{parameters.p1,jdbcType=VARCHAR} and id <= #{parameters.p2,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(8),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -961,7 +962,7 @@ void testIsNotLikeCaseInsensitiveWhenPresentWithoutValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
diff --git a/src/test/java/examples/animal/data/OptionalConditionsWithPredicatesAnimalDataTest.java b/src/test/java/examples/animal/data/OptionalConditionsWithPredicatesAnimalDataTest.java
index 7dd1e131e..4c65708cd 100644
--- a/src/test/java/examples/animal/data/OptionalConditionsWithPredicatesAnimalDataTest.java
+++ b/src/test/java/examples/animal/data/OptionalConditionsWithPredicatesAnimalDataTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -20,6 +20,7 @@
 import static examples.animal.data.AnimalDataDynamicSqlSupport.bodyWeight;
 import static examples.animal.data.AnimalDataDynamicSqlSupport.brainWeight;
 import static examples.animal.data.AnimalDataDynamicSqlSupport.id;
+import static java.util.function.Predicate.not;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertAll;
 import static org.mybatis.dynamic.sql.SqlBuilder.*;
@@ -29,7 +30,6 @@
 import java.sql.Connection;
 import java.sql.DriverManager;
 import java.util.List;
-import java.util.Objects;
 
 import org.apache.ibatis.datasource.unpooled.UnpooledDataSource;
 import org.apache.ibatis.jdbc.ScriptRunner;
@@ -39,6 +39,7 @@
 import org.apache.ibatis.session.SqlSessionFactory;
 import org.apache.ibatis.session.SqlSessionFactoryBuilder;
 import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mybatis.dynamic.sql.render.RenderingStrategies;
@@ -49,7 +50,7 @@ class OptionalConditionsWithPredicatesAnimalDataTest {
 
     private static final String JDBC_URL = "jdbc:hsqldb:mem:aname";
     private static final String JDBC_DRIVER = "org.hsqldb.jdbcDriver";
-    private static final Integer NULL_INTEGER = null;
+    private static final @Nullable Integer NULL_INTEGER = null;
 
     private SqlSessionFactory sqlSessionFactory;
 
@@ -77,7 +78,7 @@ void testAllIgnored() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(id, isGreaterThan(NULL_INTEGER).filter(Objects::nonNull))  // the where clause should not render
+                    .where(id, isGreaterThanWhenPresent(NULL_INTEGER))  // the where clause should not render
                     .orderBy(id)
                     .configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true))
                     .build()
@@ -86,8 +87,8 @@ void testAllIgnored() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData order by id"),
                     () -> assertThat(animals).hasSize(65),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1),
-                    () -> assertThat(animals).element(1).isNotNull().extracting(AnimalData::getId).isEqualTo(2)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1),
+                    () -> assertThat(animals).element(1).isNotNull().extracting(AnimalData::id).isEqualTo(2)
             );
         }
     }
@@ -99,8 +100,8 @@ void testIgnoredBetweenRendered() {
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
                     .where(id, isEqualTo(3))
-                    .and(id, isNotEqualTo(NULL_INTEGER).filter(Objects::nonNull))
-                    .or(id, isEqualTo(4).filter(Objects::nonNull))
+                    .and(id, isNotEqualToWhenPresent(NULL_INTEGER))
+                    .or(id, isEqualTo(4))
                     .orderBy(id)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -108,8 +109,8 @@ void testIgnoredBetweenRendered() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id = #{parameters.p1,jdbcType=INTEGER} or id = #{parameters.p2,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(2),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(3),
-                    () -> assertThat(animals).element(1).isNotNull().extracting(AnimalData::getId).isEqualTo(4)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(3),
+                    () -> assertThat(animals).element(1).isNotNull().extracting(AnimalData::id).isEqualTo(4)
             );
         }
     }
@@ -120,9 +121,9 @@ void testIgnoredInWhere() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(id, isLessThan(NULL_INTEGER).filter(Objects::nonNull))
-                    .and(id, isEqualTo(3).filter(Objects::nonNull))
-                    .or(id, isEqualTo(4).filter(Objects::nonNull))
+                    .where(id, isLessThanWhenPresent(NULL_INTEGER))
+                    .and(id, isEqualTo(3))
+                    .or(id, isEqualTo(4))
                     .orderBy(id)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -130,8 +131,8 @@ void testIgnoredInWhere() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id = #{parameters.p1,jdbcType=INTEGER} or id = #{parameters.p2,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(2),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(3),
-                    () -> assertThat(animals).element(1).isNotNull().extracting(AnimalData::getId).isEqualTo(4)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(3),
+                    () -> assertThat(animals).element(1).isNotNull().extracting(AnimalData::id).isEqualTo(4)
             );
         }
     }
@@ -142,10 +143,10 @@ void testManyIgnored() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(id, isLessThan(NULL_INTEGER).filter(Objects::nonNull), and(id, isGreaterThanOrEqualTo(NULL_INTEGER).filter(Objects::nonNull)))
-                    .and(id, isEqualTo(NULL_INTEGER).filter(Objects::nonNull), or(id, isEqualTo(3), and(id, isLessThan(NULL_INTEGER).filter(Objects::nonNull))))
-                    .or(id, isEqualTo(4).filter(Objects::nonNull), and(id, isGreaterThanOrEqualTo(NULL_INTEGER).filter(Objects::nonNull)))
-                    .and(id, isNotEqualTo(NULL_INTEGER).filter(Objects::nonNull))
+                    .where(id, isLessThanWhenPresent(NULL_INTEGER), and(id, isGreaterThanOrEqualToWhenPresent(NULL_INTEGER)))
+                    .and(id, isEqualToWhenPresent(NULL_INTEGER), or(id, isEqualTo(3), and(id, isLessThanWhenPresent(NULL_INTEGER))))
+                    .or(id, isEqualToWhenPresent(4), and(id, isGreaterThanOrEqualToWhenPresent(NULL_INTEGER)))
+                    .and(id, isNotEqualToWhenPresent(NULL_INTEGER))
                     .orderBy(id)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -153,8 +154,8 @@ void testManyIgnored() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id = #{parameters.p1,jdbcType=INTEGER} or id = #{parameters.p2,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(2),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(3),
-                    () -> assertThat(animals).element(1).isNotNull().extracting(AnimalData::getId).isEqualTo(4)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(3),
+                    () -> assertThat(animals).element(1).isNotNull().extracting(AnimalData::id).isEqualTo(4)
             );
         }
     }
@@ -165,8 +166,8 @@ void testIgnoredInitialWhere() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(id, isLessThan(NULL_INTEGER).filter(Objects::nonNull), and(id, isEqualTo(3).filter(Objects::nonNull)))
-                    .or(id, isEqualTo(4).filter(Objects::nonNull))
+                    .where(id, isLessThanWhenPresent(NULL_INTEGER), and(id, isEqualTo(3)))
+                    .or(id, isEqualTo(4))
                     .orderBy(id)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -174,8 +175,8 @@ void testIgnoredInitialWhere() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id = #{parameters.p1,jdbcType=INTEGER} or id = #{parameters.p2,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(2),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(3),
-                    () -> assertThat(animals).element(1).isNotNull().extracting(AnimalData::getId).isEqualTo(4)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(3),
+                    () -> assertThat(animals).element(1).isNotNull().extracting(AnimalData::id).isEqualTo(4)
             );
         }
     }
@@ -186,7 +187,7 @@ void testEqualWhenWithValue() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(id, isEqualTo(4).filter(Objects::nonNull))
+                    .where(id, isEqualTo(4))
                     .orderBy(id)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -194,7 +195,7 @@ void testEqualWhenWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id = #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(1),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(4)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(4)
             );
         }
     }
@@ -205,7 +206,7 @@ void testEqualWhenWithoutValue() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(id, isEqualTo(NULL_INTEGER).filter(Objects::nonNull))
+                    .where(id, isEqualToWhenPresent(NULL_INTEGER))
                     .and(id, isLessThanOrEqualTo(10))
                     .orderBy(id)
                     .build()
@@ -214,7 +215,7 @@ void testEqualWhenWithoutValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -225,7 +226,7 @@ void testNotEqualWhenWithValue() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(id, isNotEqualTo(4).filter(Objects::nonNull))
+                    .where(id, isNotEqualTo(4))
                     .and(id, isLessThanOrEqualTo(10))
                     .orderBy(id)
                     .build()
@@ -234,7 +235,7 @@ void testNotEqualWhenWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <> #{parameters.p1,jdbcType=INTEGER} and id <= #{parameters.p2,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(9),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -245,7 +246,7 @@ void testNotEqualWhenWithoutValue() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(id, isNotEqualTo(NULL_INTEGER).filter(Objects::nonNull))
+                    .where(id, isNotEqualToWhenPresent(NULL_INTEGER))
                     .and(id, isLessThanOrEqualTo(10))
                     .orderBy(id)
                     .build()
@@ -254,7 +255,7 @@ void testNotEqualWhenWithoutValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -265,7 +266,7 @@ void testGreaterThanWhenWithValue() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(id, isGreaterThan(4).filter(Objects::nonNull))
+                    .where(id, isGreaterThan(4))
                     .and(id, isLessThanOrEqualTo(10))
                     .orderBy(id)
                     .build()
@@ -274,7 +275,7 @@ void testGreaterThanWhenWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id > #{parameters.p1,jdbcType=INTEGER} and id <= #{parameters.p2,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(6),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(5)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(5)
             );
         }
     }
@@ -285,7 +286,7 @@ void testGreaterThanWhenWithoutValue() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(id, isGreaterThan(NULL_INTEGER).filter(Objects::nonNull))
+                    .where(id, isGreaterThanWhenPresent(NULL_INTEGER))
                     .and(id, isLessThanOrEqualTo(10))
                     .orderBy(id)
                     .build()
@@ -294,7 +295,7 @@ void testGreaterThanWhenWithoutValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -305,7 +306,7 @@ void testGreaterThanOrEqualToWhenWithValue() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(id, isGreaterThanOrEqualTo(4).filter(Objects::nonNull))
+                    .where(id, isGreaterThanOrEqualTo(4))
                     .and(id, isLessThanOrEqualTo(10))
                     .orderBy(id)
                     .build()
@@ -314,7 +315,7 @@ void testGreaterThanOrEqualToWhenWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id >= #{parameters.p1,jdbcType=INTEGER} and id <= #{parameters.p2,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(7),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(4)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(4)
             );
         }
     }
@@ -325,7 +326,7 @@ void testGreaterThanOrEqualToWhenWithoutValue() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(id, isGreaterThanOrEqualTo(NULL_INTEGER).filter(Objects::nonNull))
+                    .where(id, isGreaterThanOrEqualToWhenPresent(NULL_INTEGER))
                     .and(id, isLessThanOrEqualTo(10))
                     .orderBy(id)
                     .build()
@@ -334,7 +335,7 @@ void testGreaterThanOrEqualToWhenWithoutValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -345,7 +346,7 @@ void testLessThanWhenWithValue() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(id, isLessThan(4).filter(Objects::nonNull))
+                    .where(id, isLessThan(4))
                     .orderBy(id)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -353,7 +354,7 @@ void testLessThanWhenWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id < #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(3),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -364,7 +365,7 @@ void testLessThanWhenWithoutValue() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(id, isLessThan(NULL_INTEGER).filter(Objects::nonNull))
+                    .where(id, isLessThanWhenPresent(NULL_INTEGER))
                     .and(id, isLessThanOrEqualTo(10))
                     .orderBy(id)
                     .build()
@@ -373,7 +374,7 @@ void testLessThanWhenWithoutValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -384,7 +385,7 @@ void testLessThanOrEqualToWhenWithValue() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(id, isLessThanOrEqualTo(4).filter(Objects::nonNull))
+                    .where(id, isLessThanOrEqualTo(4))
                     .orderBy(id)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -392,7 +393,7 @@ void testLessThanOrEqualToWhenWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(4),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -403,7 +404,7 @@ void testLessThanOrEqualToWhenWithoutValue() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(id, isLessThanOrEqualTo(NULL_INTEGER).filter(Objects::nonNull))
+                    .where(id, isLessThanOrEqualToWhenPresent(NULL_INTEGER))
                     .and(id, isLessThanOrEqualTo(10))
                     .orderBy(id)
                     .build()
@@ -412,7 +413,7 @@ void testLessThanOrEqualToWhenWithoutValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -431,7 +432,7 @@ void testIsInWhenWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id in (#{parameters.p1,jdbcType=INTEGER},#{parameters.p2,jdbcType=INTEGER},#{parameters.p3,jdbcType=INTEGER}) order by id"),
                     () -> assertThat(animals).hasSize(3),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(4)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(4)
             );
         }
     }
@@ -442,7 +443,7 @@ void testIsInWhenWithSomeValues() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(id, isIn(3, NULL_INTEGER, 5).filter(Objects::nonNull).map(i -> i + 3))
+                    .where(id, isInWhenPresent(3, NULL_INTEGER, 5).map(i -> i + 3))
                     .orderBy(id)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -450,8 +451,8 @@ void testIsInWhenWithSomeValues() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id in (#{parameters.p1,jdbcType=INTEGER},#{parameters.p2,jdbcType=INTEGER}) order by id"),
                     () -> assertThat(animals).hasSize(2),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(6),
-                    () -> assertThat(animals).element(1).isNotNull().extracting(AnimalData::getId).isEqualTo(8)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(6),
+                    () -> assertThat(animals).element(1).isNotNull().extracting(AnimalData::id).isEqualTo(8)
             );
         }
     }
@@ -470,7 +471,7 @@ void testIsInCaseInsensitiveWhenWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where upper(animal_name) in (#{parameters.p1,jdbcType=VARCHAR},#{parameters.p2,jdbcType=VARCHAR}) order by id"),
                     () -> assertThat(animals).hasSize(2),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(4)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(4)
             );
         }
     }
@@ -481,10 +482,9 @@ void testValueStreamTransformer() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(animalName, isIn("  Mouse", "  ", null, "", "Musk shrew  ")
-                            .filter(Objects::nonNull)
+                    .where(animalName, isInWhenPresent("  Mouse", "  ", null, "", "Musk shrew  ")
                                     .map(String::trim)
-                                    .filter(st -> !st.isEmpty()))
+                                    .filter(not(String::isEmpty)))
                     .orderBy(id)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -492,7 +492,7 @@ void testValueStreamTransformer() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where animal_name in (#{parameters.p1,jdbcType=VARCHAR},#{parameters.p2,jdbcType=VARCHAR}) order by id"),
                     () -> assertThat(animals).hasSize(2),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(4)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(4)
             );
         }
     }
@@ -511,7 +511,7 @@ void testValueStreamTransformerWithCustomCondition() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where animal_name in (#{parameters.p1,jdbcType=VARCHAR},#{parameters.p2,jdbcType=VARCHAR}) order by id"),
                     () -> assertThat(animals).hasSize(2),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(4)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(4)
             );
         }
     }
@@ -531,7 +531,7 @@ void testIsInCaseInsensitiveWhenWithNoValues() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -551,7 +551,7 @@ void testIsNotInWhenWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id not in (#{parameters.p1,jdbcType=INTEGER},#{parameters.p2,jdbcType=INTEGER},#{parameters.p3,jdbcType=INTEGER}) and id <= #{parameters.p4,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(7),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -562,7 +562,7 @@ void testIsNotInWhenWithSomeValues() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(id, isNotIn(3, NULL_INTEGER, 5).filter(Objects::nonNull))
+                    .where(id, isNotInWhenPresent(3, NULL_INTEGER, 5))
                     .and(id, isLessThanOrEqualTo(10))
                     .orderBy(id)
                     .build()
@@ -571,7 +571,7 @@ void testIsNotInWhenWithSomeValues() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id not in (#{parameters.p1,jdbcType=INTEGER},#{parameters.p2,jdbcType=INTEGER}) and id <= #{parameters.p3,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(8),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -591,7 +591,7 @@ void testIsNotInCaseInsensitiveWhenWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where upper(animal_name) not in (#{parameters.p1,jdbcType=VARCHAR},#{parameters.p2,jdbcType=VARCHAR}) and id <= #{parameters.p3,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(8),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -611,7 +611,7 @@ void testIsNotInCaseInsensitiveWhenWithNoValues() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -630,7 +630,7 @@ void testIsBetweenWhenWithValues() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id between #{parameters.p1,jdbcType=INTEGER} and #{parameters.p2,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(4),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(3)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(3)
             );
         }
     }
@@ -641,7 +641,7 @@ void testIsBetweenWhenWithFirstMissing() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(id, isBetween(NULL_INTEGER).and(6).filter(Predicates.bothPresent()))
+                    .where(id, isBetweenWhenPresent(NULL_INTEGER).and(6))
                     .and(id, isLessThanOrEqualTo(10))
                     .orderBy(id)
                     .build()
@@ -650,7 +650,7 @@ void testIsBetweenWhenWithFirstMissing() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -661,7 +661,7 @@ void testIsBetweenWhenWithSecondMissing() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(id, isBetween(3).and(NULL_INTEGER).filter(Predicates.bothPresent()))
+                    .where(id, isBetweenWhenPresent(3).and(NULL_INTEGER))
                     .and(id, isLessThanOrEqualTo(10))
                     .orderBy(id)
                     .build()
@@ -670,7 +670,7 @@ void testIsBetweenWhenWithSecondMissing() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -681,7 +681,7 @@ void testIsBetweenWhenWithBothMissing() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(id, isBetween(NULL_INTEGER).and(NULL_INTEGER).filter(Predicates.bothPresent()))
+                    .where(id, isBetweenWhenPresent(NULL_INTEGER).and(NULL_INTEGER))
                     .and(id, isLessThanOrEqualTo(10))
                     .orderBy(id)
                     .build()
@@ -690,7 +690,7 @@ void testIsBetweenWhenWithBothMissing() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -710,7 +710,7 @@ void testIsNotBetweenWhenWithValues() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id not between #{parameters.p1,jdbcType=INTEGER} and #{parameters.p2,jdbcType=INTEGER} and id <= #{parameters.p3,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(6),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -721,7 +721,7 @@ void testIsNotBetweenWhenWithFirstMissing() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(id, isNotBetween(NULL_INTEGER).and(6).filter(Predicates.bothPresent()))
+                    .where(id, isNotBetweenWhenPresent(NULL_INTEGER).and(6))
                     .and(id, isLessThanOrEqualTo(10))
                     .orderBy(id)
                     .build()
@@ -730,7 +730,7 @@ void testIsNotBetweenWhenWithFirstMissing() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -741,7 +741,7 @@ void testIsNotBetweenWhenWithSecondMissing() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(id, isNotBetween(3).and(NULL_INTEGER).filter(Predicates.bothPresent()))
+                    .where(id, isNotBetweenWhenPresent(3).and(NULL_INTEGER))
                     .and(id, isLessThanOrEqualTo(10))
                     .orderBy(id)
                     .build()
@@ -750,7 +750,7 @@ void testIsNotBetweenWhenWithSecondMissing() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -761,7 +761,7 @@ void testIsNotBetweenWhenWithBothMissing() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(id, isNotBetween(NULL_INTEGER).and(NULL_INTEGER).filter(Predicates.bothPresent()))
+                    .where(id, isNotBetweenWhenPresent(NULL_INTEGER).and(NULL_INTEGER))
                     .and(id, isLessThanOrEqualTo(10))
                     .orderBy(id)
                     .build()
@@ -770,7 +770,7 @@ void testIsNotBetweenWhenWithBothMissing() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -781,7 +781,7 @@ void testIsLikeWhenWithValue() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(animalName, isLike("%mole").filter(Objects::nonNull))
+                    .where(animalName, isLike("%mole"))
                     .and(id, isLessThanOrEqualTo(10))
                     .orderBy(id)
                     .build()
@@ -790,7 +790,7 @@ void testIsLikeWhenWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where animal_name like #{parameters.p1,jdbcType=VARCHAR} and id <= #{parameters.p2,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(2),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(6)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(6)
             );
         }
     }
@@ -801,7 +801,7 @@ void testIsLikeWhenWithoutValue() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(animalName, isLike((String) null).filter(Objects::nonNull))
+                    .where(animalName, isLikeWhenPresent((String) null))
                     .and(id, isLessThanOrEqualTo(10))
                     .orderBy(id)
                     .build()
@@ -810,7 +810,7 @@ void testIsLikeWhenWithoutValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -821,7 +821,7 @@ void testIsLikeCaseInsensitiveWhenWithValue() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(animalName, isLikeCaseInsensitive("%MoLe").filter(Objects::nonNull))
+                    .where(animalName, isLikeCaseInsensitive("%MoLe"))
                     .and(id, isLessThanOrEqualTo(10))
                     .orderBy(id)
                     .build()
@@ -830,7 +830,7 @@ void testIsLikeCaseInsensitiveWhenWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where upper(animal_name) like #{parameters.p1,jdbcType=VARCHAR} and id <= #{parameters.p2,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(2),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(6)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(6)
             );
         }
     }
@@ -841,7 +841,7 @@ void testIsLikeCaseInsensitiveWhenWithoutValue() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(animalName, isLikeCaseInsensitive((String) null).filter(Objects::nonNull))
+                    .where(animalName, isLikeCaseInsensitiveWhenPresent((String) null))
                     .and(id, isLessThanOrEqualTo(10))
                     .orderBy(id)
                     .build()
@@ -850,7 +850,7 @@ void testIsLikeCaseInsensitiveWhenWithoutValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -861,7 +861,7 @@ void testIsNotLikeWhenWithValue() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(animalName, isNotLike("%mole").filter(Objects::nonNull))
+                    .where(animalName, isNotLike("%mole"))
                     .and(id, isLessThanOrEqualTo(10))
                     .orderBy(id)
                     .build()
@@ -870,7 +870,7 @@ void testIsNotLikeWhenWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where animal_name not like #{parameters.p1,jdbcType=VARCHAR} and id <= #{parameters.p2,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(8),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -881,7 +881,7 @@ void testIsNotLikeWhenWithoutValue() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(animalName, isNotLike((String) null).filter(Objects::nonNull))
+                    .where(animalName, isNotLikeWhenPresent((String) null))
                     .and(id, isLessThanOrEqualTo(10))
                     .orderBy(id)
                     .build()
@@ -890,7 +890,7 @@ void testIsNotLikeWhenWithoutValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -901,7 +901,7 @@ void testIsNotLikeCaseInsensitiveWhenWithValue() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(animalName, isNotLikeCaseInsensitive("%MoLe").filter(Objects::nonNull))
+                    .where(animalName, isNotLikeCaseInsensitive("%MoLe"))
                     .and(id, isLessThanOrEqualTo(10))
                     .orderBy(id)
                     .build()
@@ -910,7 +910,7 @@ void testIsNotLikeCaseInsensitiveWhenWithValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where upper(animal_name) not like #{parameters.p1,jdbcType=VARCHAR} and id <= #{parameters.p2,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(8),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
@@ -921,7 +921,7 @@ void testIsNotLikeCaseInsensitiveWhenWithoutValue() {
             AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
             SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
                     .from(animalData)
-                    .where(animalName, isNotLikeCaseInsensitive((String) null).filter(Objects::nonNull))
+                    .where(animalName, isNotLikeCaseInsensitiveWhenPresent((String) null))
                     .and(id, isLessThanOrEqualTo(10))
                     .orderBy(id)
                     .build()
@@ -930,7 +930,7 @@ void testIsNotLikeCaseInsensitiveWhenWithoutValue() {
             assertAll(
                     () -> assertThat(selectStatement.getSelectStatement()).isEqualTo("select id, animal_name, body_weight, brain_weight from AnimalData where id <= #{parameters.p1,jdbcType=INTEGER} order by id"),
                     () -> assertThat(animals).hasSize(10),
-                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::getId).isEqualTo(1)
+                    () -> assertThat(animals).first().isNotNull().extracting(AnimalData::id).isEqualTo(1)
             );
         }
     }
diff --git a/src/test/java/examples/animal/data/SubQueryTest.java b/src/test/java/examples/animal/data/SubQueryTest.java
index 5a683c6af..808478094 100644
--- a/src/test/java/examples/animal/data/SubQueryTest.java
+++ b/src/test/java/examples/animal/data/SubQueryTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/animal/data/VariousListConditionsTest.java b/src/test/java/examples/animal/data/VariousListConditionsTest.java
index 469b64718..1fef3d88b 100644
--- a/src/test/java/examples/animal/data/VariousListConditionsTest.java
+++ b/src/test/java/examples/animal/data/VariousListConditionsTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -34,14 +34,12 @@
 import java.io.InputStreamReader;
 import java.sql.Connection;
 import java.sql.DriverManager;
-import java.sql.SQLSyntaxErrorException;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
 import org.apache.ibatis.datasource.unpooled.UnpooledDataSource;
-import org.apache.ibatis.exceptions.PersistenceException;
 import org.apache.ibatis.jdbc.ScriptRunner;
 import org.apache.ibatis.mapping.Environment;
 import org.apache.ibatis.session.Configuration;
@@ -51,8 +49,10 @@
 import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.mybatis.dynamic.sql.exception.InvalidSqlException;
 import org.mybatis.dynamic.sql.render.RenderingStrategies;
 import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
+import org.mybatis.dynamic.sql.util.Messages;
 import org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper;
 
 class VariousListConditionsTest {
@@ -86,19 +86,18 @@ void testInWithNull() {
 
             SelectStatementProvider selectStatement = select(id, animalName)
                     .from(animalData)
-                    .where(id, isIn(2, 3, null))
+                    .where(id, isInWhenPresent(2, 3, null))
                     .orderBy(id)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
 
             assertThat(selectStatement.getSelectStatement()).isEqualTo(
                     "select id, animal_name from AnimalData where id " +
-                            "in (#{parameters.p1,jdbcType=INTEGER},#{parameters.p2,jdbcType=INTEGER},#{parameters.p3,jdbcType=INTEGER}) " +
+                            "in (#{parameters.p1,jdbcType=INTEGER},#{parameters.p2,jdbcType=INTEGER}) " +
                             "order by id"
             );
             assertThat(selectStatement.getParameters()).containsEntry("p1", 2);
             assertThat(selectStatement.getParameters()).containsEntry("p2", 3);
-            assertThat(selectStatement.getParameters()).containsEntry("p3", null);
 
             List<Map<String, Object>> rows = mapper.selectManyMappedRows(selectStatement);
             assertThat(rows).hasSize(2);
@@ -136,26 +135,15 @@ void testInWhenPresentWithNull() {
 
     @Test
     void testInWithEmptyList() {
-        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
-            CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class);
-
-            SelectStatementProvider selectStatement = select(id, animalName)
-                    .from(animalData)
-                    .where(id, isIn(Collections.emptyList()))
-                    .orderBy(id)
-                    .build()
-                    .render(RenderingStrategies.MYBATIS3);
-
-            assertThat(selectStatement.getSelectStatement()).isEqualTo(
-                    "select id, animal_name from AnimalData " +
-                            "where id in () " +
-                            "order by id"
-            );
-
-            assertThatExceptionOfType(PersistenceException.class).isThrownBy(() ->
-                mapper.selectManyMappedRows(selectStatement)
-            ).withCauseInstanceOf(SQLSyntaxErrorException.class);
-        }
+        var selectModel = select(id, animalName)
+                .from(animalData)
+                .where(id, isIn(Collections.emptyList()))
+                .orderBy(id)
+                .build();
+
+        assertThatExceptionOfType(InvalidSqlException.class)
+                .isThrownBy(() -> selectModel.render(RenderingStrategies.MYBATIS3))
+                .withMessage(Messages.getString("ERROR.44", "IsIn"));
     }
 
     @Test
@@ -182,13 +170,6 @@ void testInWhenPresentWithEmptyList() {
         }
     }
 
-    @Test
-    void testInWithNullList() {
-        assertThatExceptionOfType(NullPointerException.class).isThrownBy(() ->
-                isIn((Collection<Integer>) null)
-        );
-    }
-
     @Test
     void testInWhenPresentWithNullList() {
         try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
@@ -319,121 +300,66 @@ void testNotInCaseInsensitiveWhenPresentMap() {
 
     @Test
     void testInEventuallyEmpty() {
-        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
-            CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class);
-
-            SelectStatementProvider selectStatement = select(id, animalName)
-                    .from(animalData)
-                    .where(id, isIn(1, 2).filter(s -> false))
-                    .orderBy(id)
-                    .build()
-                    .render(RenderingStrategies.MYBATIS3);
-
-            assertThat(selectStatement.getSelectStatement()).isEqualTo(
-                    "select id, animal_name from AnimalData " +
-                            "where id in () " +
-                            "order by id"
-            );
-
-            assertThatExceptionOfType(PersistenceException.class).isThrownBy(
-                    () -> mapper.selectManyMappedRows(selectStatement))
-                    .withCauseInstanceOf(SQLSyntaxErrorException.class);
-        }
+        var selectModel = select(id, animalName)
+                .from(animalData)
+                .where(id, isIn(1, 2).filter(s -> false))
+                .orderBy(id)
+                .build();
+
+        assertThatExceptionOfType(InvalidSqlException.class)
+                .isThrownBy(() -> selectModel.render(RenderingStrategies.MYBATIS3))
+                .withMessage(Messages.getString("ERROR.44", "IsIn"));
     }
 
     @Test
     void testInCaseInsensitiveEventuallyEmpty() {
-        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
-            CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class);
-
-            SelectStatementProvider selectStatement = select(id, animalName)
-                    .from(animalData)
-                    .where(animalName, isInCaseInsensitive("Fred", "Betty").filter(s -> false))
-                    .orderBy(id)
-                    .build()
-                    .render(RenderingStrategies.MYBATIS3);
-
-            assertThat(selectStatement.getSelectStatement()).isEqualTo(
-                    "select id, animal_name from AnimalData " +
-                            "where upper(animal_name) in () " +
-                            "order by id"
-            );
-
-            assertThatExceptionOfType(PersistenceException.class).isThrownBy(
-                            () -> mapper.selectManyMappedRows(selectStatement))
-                    .withCauseInstanceOf(SQLSyntaxErrorException.class);
-        }
+        var selectModel = select(id, animalName)
+                .from(animalData)
+                .where(animalName, isInCaseInsensitive("Fred", "Betty").filter(s -> false))
+                .orderBy(id)
+                .build();
+
+        assertThatExceptionOfType(InvalidSqlException.class)
+                .isThrownBy(() -> selectModel.render(RenderingStrategies.MYBATIS3))
+                .withMessage(Messages.getString("ERROR.44", "IsInCaseInsensitive"));
     }
 
     @Test
     void testNotInEventuallyEmpty() {
-        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
-            CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class);
-
-            SelectStatementProvider selectStatement = select(id, animalName)
-                    .from(animalData)
-                    .where(id, isNotIn(1, 2).filter(s -> false))
-                    .orderBy(id)
-                    .build()
-                    .render(RenderingStrategies.MYBATIS3);
-
-            assertThat(selectStatement.getSelectStatement()).isEqualTo(
-                    "select id, animal_name from AnimalData " +
-                            "where id not in () " +
-                            "order by id"
-            );
-
-            assertThatExceptionOfType(PersistenceException.class).isThrownBy(
-                            () -> mapper.selectManyMappedRows(selectStatement))
-                    .withCauseInstanceOf(SQLSyntaxErrorException.class);
-        }
+        var selectModel = select(id, animalName)
+                .from(animalData)
+                .where(id, isNotIn(1, 2).filter(s -> false))
+                .orderBy(id)
+                .build();
+
+        assertThatExceptionOfType(InvalidSqlException.class)
+                .isThrownBy(() -> selectModel.render(RenderingStrategies.MYBATIS3))
+                .withMessage(Messages.getString("ERROR.44", "IsNotIn"));
     }
 
     @Test
     void testNotInCaseInsensitiveEventuallyEmpty() {
-        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
-            CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class);
-
-            SelectStatementProvider selectStatement = select(id, animalName)
-                    .from(animalData)
-                    .where(animalName, isNotInCaseInsensitive("Fred", "Betty").filter(s -> false))
-                    .orderBy(id)
-                    .build()
-                    .render(RenderingStrategies.MYBATIS3);
-
-            assertThat(selectStatement.getSelectStatement()).isEqualTo(
-                    "select id, animal_name from AnimalData " +
-                            "where upper(animal_name) not in () " +
-                            "order by id"
-            );
-
-            assertThatExceptionOfType(PersistenceException.class).isThrownBy(
-                            () -> mapper.selectManyMappedRows(selectStatement))
-                    .withCauseInstanceOf(SQLSyntaxErrorException.class);
-        }
+        var selectModel = select(id, animalName)
+                .from(animalData)
+                .where(animalName, isNotInCaseInsensitive("Fred", "Betty").filter(s -> false))
+                .orderBy(id)
+                .build();
+
+        assertThatExceptionOfType(InvalidSqlException.class)
+                .isThrownBy(() -> selectModel.render(RenderingStrategies.MYBATIS3))
+                .withMessage(Messages.getString("ERROR.44", "IsNotInCaseInsensitive"));
     }
 
     @Test
     void testInEventuallyEmptyDoubleFilter() {
-        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
-            CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class);
-
-            SelectStatementProvider selectStatement = select(id, animalName)
-                    .from(animalData)
-                    .where(id, isIn(1, 2).filter(s -> false).filter(s -> false))
-                    .orderBy(id)
-                    .build()
-                    .render(RenderingStrategies.MYBATIS3);
-
-            assertThat(selectStatement.getSelectStatement()).isEqualTo(
-                    "select id, animal_name from AnimalData " +
-                            "where id in () " +
-                            "order by id"
-            );
-
-            assertThatExceptionOfType(PersistenceException.class).isThrownBy(
-                            () -> mapper.selectManyMappedRows(selectStatement))
-                    .withCauseInstanceOf(SQLSyntaxErrorException.class);
-        }
+        var selectModel = select(id, animalName)
+                .from(animalData)
+                .where(id, isIn(1, 2).filter(s -> false).filter(s -> false))
+                .orderBy(id)
+                .build();
+
+        assertThatExceptionOfType(InvalidSqlException.class)
+                .isThrownBy(() -> selectModel.render(RenderingStrategies.MYBATIS3))
+                .withMessage(Messages.getString("ERROR.44", "IsIn"));
     }
 }
diff --git a/src/test/java/examples/animal/data/VariousPagingAndLimitScenariosTest.java b/src/test/java/examples/animal/data/VariousPagingAndLimitScenariosTest.java
new file mode 100644
index 000000000..8f368994a
--- /dev/null
+++ b/src/test/java/examples/animal/data/VariousPagingAndLimitScenariosTest.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 examples.animal.data;
+
+import static examples.animal.data.AnimalDataDynamicSqlSupport.animalData;
+import static examples.animal.data.AnimalDataDynamicSqlSupport.id;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mybatis.dynamic.sql.SqlBuilder.deleteFrom;
+import static org.mybatis.dynamic.sql.SqlBuilder.isLessThan;
+import static org.mybatis.dynamic.sql.SqlBuilder.select;
+import static org.mybatis.dynamic.sql.SqlBuilder.update;
+
+import org.junit.jupiter.api.Test;
+import org.mybatis.dynamic.sql.render.RenderingStrategies;
+
+class VariousPagingAndLimitScenariosTest {
+
+    @Test
+    void testOptionalLimitOnDelete() {
+        var deleteStatement = deleteFrom(animalData)
+                .limitWhenPresent(null)
+                .build()
+                .render(RenderingStrategies.SPRING_NAMED_PARAMETER);
+
+        assertThat(deleteStatement.getDeleteStatement()).isEqualTo("delete from AnimalData");
+    }
+
+    @Test
+    void testOptionalLimitOnDeleteWithWhere() {
+        var deleteStatement = deleteFrom(animalData)
+                .where(id, isLessThan(22))
+                .limitWhenPresent(null)
+                .build()
+                .render(RenderingStrategies.SPRING_NAMED_PARAMETER);
+
+        assertThat(deleteStatement.getDeleteStatement())
+                .isEqualTo("delete from AnimalData where id < :p1");
+    }
+
+    @Test
+    void testOptionalLimitOnUpdate() {
+        var updateStatement = update(animalData)
+                .set(id).equalTo(1)
+                .limitWhenPresent(null)
+                .build()
+                .render(RenderingStrategies.SPRING_NAMED_PARAMETER);
+
+        assertThat(updateStatement.getUpdateStatement()).isEqualTo("update AnimalData set id = :p1");
+    }
+
+    @Test
+    void testOptionalLimitOnUpdateWithWhere() {
+        var updateStatement = update(animalData)
+                .set(id).equalTo(1)
+                .where(id, isLessThan(22))
+                .limitWhenPresent(null)
+                .build()
+                .render(RenderingStrategies.SPRING_NAMED_PARAMETER);
+
+        assertThat(updateStatement.getUpdateStatement()).isEqualTo("update AnimalData set id = :p1 where id < :p2");
+   }
+
+    @Test
+    void testOptionalLimitOnSelect() {
+        var selectStatement = select(animalData.allColumns())
+                .from(animalData)
+                .limitWhenPresent(null)
+                .build()
+                .render(RenderingStrategies.SPRING_NAMED_PARAMETER);
+
+        assertThat(selectStatement.getSelectStatement()).isEqualTo("select * from AnimalData");
+    }
+
+    @Test
+    void testOptionalOffsetOnSelect() {
+        var selectStatement = select(animalData.allColumns())
+                .from(animalData)
+                .offsetWhenPresent(null)
+                .build()
+                .render(RenderingStrategies.SPRING_NAMED_PARAMETER);
+
+        assertThat(selectStatement.getSelectStatement()).isEqualTo("select * from AnimalData");
+    }
+
+    @Test
+    void testOptionalFetchFirstOnSelect() {
+        var selectStatement = select(animalData.allColumns())
+                .from(animalData)
+                .fetchFirstWhenPresent(null).rowsOnly()
+                .build()
+                .render(RenderingStrategies.SPRING_NAMED_PARAMETER);
+
+        assertThat(selectStatement.getSelectStatement()).isEqualTo("select * from AnimalData");
+    }
+
+    @Test
+    void testOptionalLimitAndOffsetOnSelect() {
+        var selectStatement = select(animalData.allColumns())
+                .from(animalData)
+                .limitWhenPresent(null)
+                .offsetWhenPresent(null)
+                .build()
+                .render(RenderingStrategies.SPRING_NAMED_PARAMETER);
+
+        assertThat(selectStatement.getSelectStatement()).isEqualTo("select * from AnimalData");
+    }
+
+    @Test
+    void testOptionalOffsetAndFetchOnSelect() {
+        var selectStatement = select(animalData.allColumns())
+                .from(animalData)
+                .offsetWhenPresent(null)
+                .fetchFirstWhenPresent(null).rowsOnly()
+                .build()
+                .render(RenderingStrategies.SPRING_NAMED_PARAMETER);
+
+        assertThat(selectStatement.getSelectStatement()).isEqualTo("select * from AnimalData");
+    }
+}
diff --git a/src/test/java/examples/animal/data/package-info.java b/src/test/java/examples/animal/data/package-info.java
new file mode 100644
index 000000000..0b0ff393f
--- /dev/null
+++ b/src/test/java/examples/animal/data/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 examples.animal.data;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/src/test/java/examples/array/ArrayTest.java b/src/test/java/examples/array/ArrayTest.java
index 1890c1821..623221e41 100644
--- a/src/test/java/examples/array/ArrayTest.java
+++ b/src/test/java/examples/array/ArrayTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/array/NamesRecord.java b/src/test/java/examples/array/NamesRecord.java
index 752999f86..65765ccfe 100644
--- a/src/test/java/examples/array/NamesRecord.java
+++ b/src/test/java/examples/array/NamesRecord.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/array/NamesTableDynamicSqlSupport.java b/src/test/java/examples/array/NamesTableDynamicSqlSupport.java
index cd40201f6..ce3cfbb8f 100644
--- a/src/test/java/examples/array/NamesTableDynamicSqlSupport.java
+++ b/src/test/java/examples/array/NamesTableDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/array/NamesTableMapper.java b/src/test/java/examples/array/NamesTableMapper.java
index b8fcc0e80..a42509d42 100644
--- a/src/test/java/examples/array/NamesTableMapper.java
+++ b/src/test/java/examples/array/NamesTableMapper.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/array/StringArrayTypeHandler.java b/src/test/java/examples/array/StringArrayTypeHandler.java
index 50f2d2907..555542091 100644
--- a/src/test/java/examples/array/StringArrayTypeHandler.java
+++ b/src/test/java/examples/array/StringArrayTypeHandler.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -22,7 +22,6 @@
 import java.sql.SQLException;
 import java.util.Arrays;
 import java.util.List;
-import java.util.stream.Collectors;
 
 import org.apache.ibatis.type.BaseTypeHandler;
 import org.apache.ibatis.type.JdbcType;
@@ -59,7 +58,7 @@ private String[] extractArray(Array array) throws SQLException {
 
         List<String> stringList = Arrays.stream(objArray)
                 .map(Object::toString)
-                .collect(Collectors.toList());
+                .toList();
 
         String[] stringArray = new String[stringList.size()];
         return stringList.toArray(stringArray);
diff --git a/src/test/java/examples/column/comparison/ColumnComparisonConfiguration.java b/src/test/java/examples/column/comparison/ColumnComparisonConfiguration.java
index 1a216bf0d..12bb66cf3 100644
--- a/src/test/java/examples/column/comparison/ColumnComparisonConfiguration.java
+++ b/src/test/java/examples/column/comparison/ColumnComparisonConfiguration.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/column/comparison/ColumnComparisonDynamicSqlSupport.java b/src/test/java/examples/column/comparison/ColumnComparisonDynamicSqlSupport.java
index efc8cf83d..f1289b000 100644
--- a/src/test/java/examples/column/comparison/ColumnComparisonDynamicSqlSupport.java
+++ b/src/test/java/examples/column/comparison/ColumnComparisonDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/column/comparison/ColumnComparisonMapper.java b/src/test/java/examples/column/comparison/ColumnComparisonMapper.java
index 6d2860bc2..dc27de8d0 100644
--- a/src/test/java/examples/column/comparison/ColumnComparisonMapper.java
+++ b/src/test/java/examples/column/comparison/ColumnComparisonMapper.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,8 +17,7 @@
 
 import java.util.List;
 
-import org.apache.ibatis.annotations.Result;
-import org.apache.ibatis.annotations.Results;
+import org.apache.ibatis.annotations.Arg;
 import org.apache.ibatis.annotations.SelectProvider;
 import org.mybatis.dynamic.sql.select.SelectDSLCompleter;
 import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
@@ -28,10 +27,8 @@
 public interface ColumnComparisonMapper {
 
     @SelectProvider(type=SqlProviderAdapter.class, method="select")
-    @Results({
-        @Result(column="number1", property="number1", id=true),
-        @Result(column="number2", property="number2", id=true)
-    })
+    @Arg(column="number1", javaType = int.class, id=true)
+    @Arg(column="number2", javaType = int.class, id=true)
     List<ColumnComparisonRecord> selectMany(SelectStatementProvider selectStatement);
 
     default List<ColumnComparisonRecord> select(SelectDSLCompleter completer) {
diff --git a/src/test/java/examples/column/comparison/ColumnComparisonRecord.java b/src/test/java/examples/column/comparison/ColumnComparisonRecord.java
index 4ad85d81b..8222ca042 100644
--- a/src/test/java/examples/column/comparison/ColumnComparisonRecord.java
+++ b/src/test/java/examples/column/comparison/ColumnComparisonRecord.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,23 +15,4 @@
  */
 package examples.column.comparison;
 
-public class ColumnComparisonRecord {
-    private int number1;
-    private int number2;
-
-    public int getNumber1() {
-        return number1;
-    }
-
-    public void setNumber1(int number1) {
-        this.number1 = number1;
-    }
-
-    public int getNumber2() {
-        return number2;
-    }
-
-    public void setNumber2(int number2) {
-        this.number2 = number2;
-    }
-}
+public record ColumnComparisonRecord (int number1, int number2) {}
diff --git a/src/test/java/examples/column/comparison/ColumnComparisonTest.java b/src/test/java/examples/column/comparison/ColumnComparisonTest.java
index e70e4211b..746db94cd 100644
--- a/src/test/java/examples/column/comparison/ColumnComparisonTest.java
+++ b/src/test/java/examples/column/comparison/ColumnComparisonTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -51,8 +51,8 @@ void testColumnComparisonLessThan() {
 
         assertThat(selectStatement.getSelectStatement()).isEqualTo(expected);
         assertThat(records).hasSize(5);
-        assertThat(records.get(0).getNumber1()).isEqualTo(1);
-        assertThat(records.get(4).getNumber1()).isEqualTo(5);
+        assertThat(records.get(0).number1()).isEqualTo(1);
+        assertThat(records.get(4).number1()).isEqualTo(5);
     }
 
     @Test
@@ -73,8 +73,8 @@ void testColumnComparisonLessThanOrEqual() {
 
         assertThat(selectStatement.getSelectStatement()).isEqualTo(expected);
         assertThat(records).hasSize(6);
-        assertThat(records.get(0).getNumber1()).isEqualTo(1);
-        assertThat(records.get(5).getNumber1()).isEqualTo(6);
+        assertThat(records.get(0).number1()).isEqualTo(1);
+        assertThat(records.get(5).number2()).isEqualTo(6);
     }
 
     @Test
@@ -95,8 +95,8 @@ void testColumnComparisonGreaterThan() {
 
         assertThat(selectStatement.getSelectStatement()).isEqualTo(expected);
         assertThat(records).hasSize(5);
-        assertThat(records.get(0).getNumber1()).isEqualTo(7);
-        assertThat(records.get(4).getNumber1()).isEqualTo(11);
+        assertThat(records.get(0).number1()).isEqualTo(7);
+        assertThat(records.get(4).number1()).isEqualTo(11);
     }
 
     @Test
@@ -117,8 +117,8 @@ void testColumnComparisonGreaterThanOrEqual() {
 
         assertThat(selectStatement.getSelectStatement()).isEqualTo(expected);
         assertThat(records).hasSize(6);
-        assertThat(records.get(0).getNumber1()).isEqualTo(6);
-        assertThat(records.get(5).getNumber1()).isEqualTo(11);
+        assertThat(records.get(0).number1()).isEqualTo(6);
+        assertThat(records.get(5).number1()).isEqualTo(11);
     }
 
     @Test
@@ -139,7 +139,7 @@ void testColumnComparisonEqual() {
 
         assertThat(selectStatement.getSelectStatement()).isEqualTo(expected);
         assertThat(records).hasSize(1);
-        assertThat(records.get(0).getNumber1()).isEqualTo(6);
+        assertThat(records.get(0).number1()).isEqualTo(6);
     }
 
     @Test
@@ -160,8 +160,8 @@ void testColumnComparisonNotEqual() {
 
         assertThat(selectStatement.getSelectStatement()).isEqualTo(expected);
         assertThat(records).hasSize(10);
-        assertThat(records.get(0).getNumber1()).isEqualTo(1);
-        assertThat(records.get(9).getNumber1()).isEqualTo(11);
+        assertThat(records.get(0).number1()).isEqualTo(1);
+        assertThat(records.get(9).number1()).isEqualTo(11);
     }
 
     @Test
@@ -172,7 +172,7 @@ void testHelperMethod() {
         );
 
         assertThat(records).hasSize(10);
-        assertThat(records.get(0).getNumber1()).isEqualTo(1);
-        assertThat(records.get(9).getNumber1()).isEqualTo(11);
+        assertThat(records.get(0).number1()).isEqualTo(1);
+        assertThat(records.get(9).number1()).isEqualTo(11);
     }
 }
diff --git a/src/test/java/examples/complexquery/ComplexQueryTest.java b/src/test/java/examples/complexquery/ComplexQueryTest.java
index 2b46b6845..e31ab536b 100644
--- a/src/test/java/examples/complexquery/ComplexQueryTest.java
+++ b/src/test/java/examples/complexquery/ComplexQueryTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -22,8 +22,7 @@
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mybatis.dynamic.sql.SqlBuilder.*;
 
-import java.util.Objects;
-
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.Test;
 import org.mybatis.dynamic.sql.render.RenderingStrategies;
 import org.mybatis.dynamic.sql.select.QueryExpressionDSL;
@@ -107,19 +106,18 @@ void testAllNull() {
         assertThat(selectStatement.getParameters()).containsEntry("p1", 50L);
     }
 
-    SelectStatementProvider search(Integer targetId, String fName, String lName) {
+    SelectStatementProvider search(@Nullable Integer targetId, @Nullable String fName, @Nullable String lName) {
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder = select(id, firstName, lastName)
                 .from(person)
                 .where()
                 .configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true));
 
         if (targetId != null) {
-            builder
-                .and(id, isEqualTo(targetId));
+            builder.and(id, isEqualTo(targetId));
         } else {
             builder
-                .and(firstName, isLike(fName).filter(Objects::nonNull).map(s -> "%" + s + "%"))
-                .and(lastName, isLikeWhenPresent(lName).map(this::addWildcards));
+                .and(firstName, isLikeWhenPresent(fName).map(s -> "%" + s + "%"))
+                .and(lastName, isLikeWhenPresent(lName).map(SearchUtils::addWildcards));
         }
 
         builder
@@ -128,8 +126,4 @@ SelectStatementProvider search(Integer targetId, String fName, String lName) {
 
         return builder.build().render(RenderingStrategies.MYBATIS3);
     }
-
-    String addWildcards(String s) {
-        return "%" + s + "%";
-    }
 }
diff --git a/src/test/java/examples/complexquery/GroupingTest.java b/src/test/java/examples/complexquery/GroupingTest.java
index c7e09c467..b6552b3cb 100644
--- a/src/test/java/examples/complexquery/GroupingTest.java
+++ b/src/test/java/examples/complexquery/GroupingTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -33,9 +33,9 @@
 
 class GroupingTest {
     private static class Foo extends SqlTable {
-        public SqlColumn<Integer> A = column("A");
-        public SqlColumn<Integer> B = column("B");
-        public SqlColumn<Integer> C = column("C");
+        public final SqlColumn<Integer> columnA = column("A");
+        public final SqlColumn<Integer> columnB = column("B");
+        public final SqlColumn<Integer> columnC = column("C");
 
         public Foo() {
             super("Foo");
@@ -43,16 +43,16 @@ public Foo() {
     }
 
     private static final Foo foo = new Foo();
-    private static final SqlColumn<Integer> A = foo.A;
-    private static final SqlColumn<Integer> B = foo.B;
-    private static final SqlColumn<Integer> C = foo.C;
+    private static final SqlColumn<Integer> columnA = foo.columnA;
+    private static final SqlColumn<Integer> columnB = foo.columnB;
+    private static final SqlColumn<Integer> columnC = foo.columnC;
 
     @Test
     void testSimpleGrouping() {
-        SelectStatementProvider selectStatement = select(A, B, C)
+        SelectStatementProvider selectStatement = select(columnA, columnB, columnC)
                 .from(foo)
-                .where(A, isEqualTo(1), or(A, isEqualTo(2)))
-                .and(B, isEqualTo(3))
+                .where(columnA, isEqualTo(1), or(columnA, isEqualTo(2)))
+                .and(columnB, isEqualTo(3))
                 .build()
                 .render(RenderingStrategies.MYBATIS3);
 
@@ -68,14 +68,14 @@ void testSimpleGrouping() {
 
     @Test
     void testComplexGrouping() {
-        SelectStatementProvider selectStatement = select(A, B, C)
+        SelectStatementProvider selectStatement = select(columnA, columnB, columnC)
                 .from(foo)
                 .where(
-                        group(A, isEqualTo(1), or(A, isGreaterThan(5))),
-                        and(B, isEqualTo(1)),
-                        or(A, isLessThan(0), and(B, isEqualTo(2)))
+                        group(columnA, isEqualTo(1), or(columnA, isGreaterThan(5))),
+                        and(columnB, isEqualTo(1)),
+                        or(columnA, isLessThan(0), and(columnB, isEqualTo(2)))
                 )
-                .and(C, isEqualTo(1))
+                .and(columnC, isEqualTo(1))
                 .build()
                 .render(RenderingStrategies.MYBATIS3);
 
@@ -94,14 +94,14 @@ void testComplexGrouping() {
 
     @Test
     void testGroupAndExists() {
-        SelectStatementProvider selectStatement = select(A, B, C)
+        SelectStatementProvider selectStatement = select(columnA, columnB, columnC)
                 .from(foo)
                 .where(
-                        group(exists(select(foo.allColumns()).from(foo).where(A, isEqualTo(3))), and (A, isEqualTo(1)), or(A, isGreaterThan(5))),
-                        and(B, isEqualTo(1)),
-                        or(A, isLessThan(0), and(B, isEqualTo(2)))
+                        group(exists(select(foo.allColumns()).from(foo).where(columnA, isEqualTo(3))), and (columnA, isEqualTo(1)), or(columnA, isGreaterThan(5))),
+                        and(columnB, isEqualTo(1)),
+                        or(columnA, isLessThan(0), and(columnB, isEqualTo(2)))
                 )
-                .and(C, isEqualTo(1))
+                .and(columnC, isEqualTo(1))
                 .build()
                 .render(RenderingStrategies.MYBATIS3);
 
@@ -121,14 +121,14 @@ void testGroupAndExists() {
 
     @Test
     void testNestedGrouping() {
-        SelectStatementProvider selectStatement = select(A, B, C)
+        SelectStatementProvider selectStatement = select(columnA, columnB, columnC)
                 .from(foo)
                 .where(
-                        group(group(A, isEqualTo(1), or(A, isGreaterThan(5))), and(A, isGreaterThan(5))),
-                        and(group(A, isEqualTo(1), or(A, isGreaterThan(5))), or(B, isEqualTo(1))),
-                        or(group(A, isEqualTo(1), or(A, isGreaterThan(5))), and(A, isLessThan(0), and(B, isEqualTo(2))))
+                        group(group(columnA, isEqualTo(1), or(columnA, isGreaterThan(5))), and(columnA, isGreaterThan(5))),
+                        and(group(columnA, isEqualTo(1), or(columnA, isGreaterThan(5))), or(columnB, isEqualTo(1))),
+                        or(group(columnA, isEqualTo(1), or(columnA, isGreaterThan(5))), and(columnA, isLessThan(0), and(columnB, isEqualTo(2))))
                 )
-                .and(C, isEqualTo(1))
+                .and(columnC, isEqualTo(1))
                 .build()
                 .render(RenderingStrategies.MYBATIS3);
 
@@ -152,12 +152,12 @@ void testNestedGrouping() {
 
     @Test
     void testAndOrCriteriaGroups() {
-        SelectStatementProvider selectStatement = select(A, B, C)
+        SelectStatementProvider selectStatement = select(columnA, columnB, columnC)
                 .from(foo)
-                .where(A, isEqualTo(6))
-                .and(C, isEqualTo(1))
-                .and(group(A, isEqualTo(1), or(A, isGreaterThan(5))), or(B, isEqualTo(1)))
-                .or(group(A, isEqualTo(1), or(A, isGreaterThan(5))), and(A, isLessThan(0), and(B, isEqualTo(2))))
+                .where(columnA, isEqualTo(6))
+                .and(columnC, isEqualTo(1))
+                .and(group(columnA, isEqualTo(1), or(columnA, isGreaterThan(5))), or(columnB, isEqualTo(1)))
+                .or(group(columnA, isEqualTo(1), or(columnA, isGreaterThan(5))), and(columnA, isLessThan(0), and(columnB, isEqualTo(2))))
                 .build()
                 .render(RenderingStrategies.MYBATIS3);
 
diff --git a/src/test/java/examples/complexquery/PersonDynamicSqlSupport.java b/src/test/java/examples/complexquery/PersonDynamicSqlSupport.java
index e206d739b..900593419 100644
--- a/src/test/java/examples/complexquery/PersonDynamicSqlSupport.java
+++ b/src/test/java/examples/complexquery/PersonDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/complexquery/SearchUtils.java b/src/test/java/examples/complexquery/SearchUtils.java
index 773520437..92b40d88b 100644
--- a/src/test/java/examples/complexquery/SearchUtils.java
+++ b/src/test/java/examples/complexquery/SearchUtils.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/custom_render/CustomRenderingTest.java b/src/test/java/examples/custom_render/CustomRenderingTest.java
index 8ca34bbc2..8b0541c1e 100644
--- a/src/test/java/examples/custom_render/CustomRenderingTest.java
+++ b/src/test/java/examples/custom_render/CustomRenderingTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -40,7 +40,7 @@
 import org.apache.ibatis.session.SqlSessionFactory;
 import org.apache.ibatis.session.SqlSessionFactoryBuilder;
 import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
-import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mybatis.dynamic.sql.SqlColumn;
 import org.mybatis.dynamic.sql.insert.render.GeneralInsertStatementProvider;
@@ -63,10 +63,10 @@ class CustomRenderingTest {
             new PostgreSQLContainer<>(TestContainersConfiguration.POSTGRES_LATEST)
                     .withInitScript("examples/custom_render/dbInit.sql");
 
-    private static SqlSessionFactory sqlSessionFactory;
+    private SqlSessionFactory sqlSessionFactory;
 
-    @BeforeAll
-    static void setUp() {
+    @BeforeEach
+    void setUp() {
         UnpooledDataSource ds = new UnpooledDataSource(postgres.getDriverClassName(), postgres.getJdbcUrl(),
                 postgres.getUsername(), postgres.getPassword());
         Environment environment = new Environment("test", new JdbcTransactionFactory(), ds);
@@ -81,10 +81,8 @@ void testInsertRecord() {
         try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
             JsonTestMapper mapper = sqlSession.getMapper(JsonTestMapper.class);
 
-            JsonTestRecord row = new JsonTestRecord();
-            row.setId(1);
-            row.setDescription("Fred");
-            row.setInfo("{\"firstName\": \"Fred\", \"lastName\": \"Flintstone\", \"age\": 30}");
+            JsonTestRecord row = new JsonTestRecord(1, "Fred",
+                    "{\"firstName\": \"Fred\", \"lastName\": \"Flintstone\", \"age\": 30}");
 
             InsertStatementProvider<JsonTestRecord> insertStatement = insert(row).into(jsonTest)
                     .map(id).toProperty("id")
@@ -102,10 +100,8 @@ void testInsertRecord() {
             int rows = mapper.insert(insertStatement);
             assertThat(rows).isEqualTo(1);
 
-            row = new JsonTestRecord();
-            row.setId(2);
-            row.setDescription("Wilma");
-            row.setInfo("{\"firstName\": \"Wilma\", \"lastName\": \"Flintstone\", \"age\": 25}");
+            row = new JsonTestRecord(2, "Wilma",
+                    "{\"firstName\": \"Wilma\", \"lastName\": \"Flintstone\", \"age\": 25}");
 
             insertStatement = insert(row).into(jsonTest)
                     .map(id).toProperty("id")
@@ -125,8 +121,8 @@ void testInsertRecord() {
 
             List<JsonTestRecord> records = mapper.selectMany(selectStatement);
             assertThat(records).hasSize(2);
-            assertThat(records.get(0).getInfo()).isEqualTo("{\"firstName\": \"Fred\", \"lastName\": \"Flintstone\", \"age\": 30}");
-            assertThat(records.get(1).getInfo()).isEqualTo("{\"firstName\": \"Wilma\", \"lastName\": \"Flintstone\", \"age\": 25}");
+            assertThat(records.get(0).info()).isEqualTo("{\"firstName\": \"Fred\", \"lastName\": \"Flintstone\", \"age\": 30}");
+            assertThat(records.get(1).info()).isEqualTo("{\"firstName\": \"Wilma\", \"lastName\": \"Flintstone\", \"age\": 25}");
         }
     }
 
@@ -169,8 +165,8 @@ void testGeneralInsert() {
 
             List<JsonTestRecord> records = mapper.selectMany(selectStatement);
             assertThat(records).hasSize(2);
-            assertThat(records.get(0).getInfo()).isEqualTo("{\"firstName\": \"Fred\", \"lastName\": \"Flintstone\", \"age\": 30}");
-            assertThat(records.get(1).getInfo()).isEqualTo("{\"firstName\": \"Wilma\", \"lastName\": \"Flintstone\", \"age\": 25}");
+            assertThat(records.get(0).info()).isEqualTo("{\"firstName\": \"Fred\", \"lastName\": \"Flintstone\", \"age\": 30}");
+            assertThat(records.get(1).info()).isEqualTo("{\"firstName\": \"Wilma\", \"lastName\": \"Flintstone\", \"age\": 25}");
         }
     }
 
@@ -179,15 +175,11 @@ void testInsertMultiple() {
         try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
             JsonTestMapper mapper = sqlSession.getMapper(JsonTestMapper.class);
 
-            JsonTestRecord record1 = new JsonTestRecord();
-            record1.setId(1);
-            record1.setDescription("Fred");
-            record1.setInfo("{\"firstName\": \"Fred\", \"lastName\": \"Flintstone\", \"age\": 30}");
+            JsonTestRecord record1 = new JsonTestRecord(1, "Fred",
+                    "{\"firstName\": \"Fred\", \"lastName\": \"Flintstone\", \"age\": 30}");
 
-            JsonTestRecord record2 = new JsonTestRecord();
-            record2.setId(2);
-            record2.setDescription("Wilma");
-            record2.setInfo("{\"firstName\": \"Wilma\", \"lastName\": \"Flintstone\", \"age\": 25}");
+            JsonTestRecord record2 = new JsonTestRecord(2, "Wilma",
+                    "{\"firstName\": \"Wilma\", \"lastName\": \"Flintstone\", \"age\": 25}");
 
             MultiRowInsertStatementProvider<JsonTestRecord> insertStatement = insertMultiple(record1, record2)
                     .into(jsonTest)
@@ -216,8 +208,8 @@ void testInsertMultiple() {
 
             List<JsonTestRecord> records = mapper.selectMany(selectStatement);
             assertThat(records).hasSize(2);
-            assertThat(records.get(0).getInfo()).isEqualTo("{\"firstName\": \"Fred\", \"lastName\": \"Flintstone\", \"age\": 30}");
-            assertThat(records.get(1).getInfo()).isEqualTo("{\"firstName\": \"Wilma\", \"lastName\": \"Flintstone\", \"age\": 25}");
+            assertThat(records.get(0).info()).isEqualTo("{\"firstName\": \"Fred\", \"lastName\": \"Flintstone\", \"age\": 30}");
+            assertThat(records.get(1).info()).isEqualTo("{\"firstName\": \"Wilma\", \"lastName\": \"Flintstone\", \"age\": 25}");
         }
     }
 
@@ -226,15 +218,11 @@ void testUpdate() {
         try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
             JsonTestMapper mapper = sqlSession.getMapper(JsonTestMapper.class);
 
-            JsonTestRecord record1 = new JsonTestRecord();
-            record1.setId(1);
-            record1.setDescription("Fred");
-            record1.setInfo("{\"firstName\": \"Fred\", \"lastName\": \"Flintstone\", \"age\": 30}");
+            JsonTestRecord record1 = new JsonTestRecord(1, "Fred",
+                    "{\"firstName\": \"Fred\", \"lastName\": \"Flintstone\", \"age\": 30}");
 
-            JsonTestRecord record2 = new JsonTestRecord();
-            record2.setId(2);
-            record2.setDescription("Wilma");
-            record2.setInfo("{\"firstName\": \"Wilma\", \"lastName\": \"Flintstone\", \"age\": 25}");
+            JsonTestRecord record2 = new JsonTestRecord(2, "Wilma",
+                    "{\"firstName\": \"Wilma\", \"lastName\": \"Flintstone\", \"age\": 25}");
 
             MultiRowInsertStatementProvider<JsonTestRecord> insertStatement = insertMultiple(record1, record2)
                     .into(jsonTest)
@@ -270,8 +258,8 @@ void testUpdate() {
 
             List<JsonTestRecord> records = mapper.selectMany(selectStatement);
             assertThat(records).hasSize(2);
-            assertThat(records.get(0).getInfo()).isEqualTo("{\"firstName\": \"Fred\", \"lastName\": \"Flintstone\", \"age\": 30}");
-            assertThat(records.get(1).getInfo()).isEqualTo("{\"firstName\": \"Wilma\", \"lastName\": \"Flintstone\", \"age\": 26}");
+            assertThat(records.get(0).info()).isEqualTo("{\"firstName\": \"Fred\", \"lastName\": \"Flintstone\", \"age\": 30}");
+            assertThat(records.get(1).info()).isEqualTo("{\"firstName\": \"Wilma\", \"lastName\": \"Flintstone\", \"age\": 26}");
         }
     }
 
@@ -280,15 +268,11 @@ void testDeReference() {
         try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
             JsonTestMapper mapper = sqlSession.getMapper(JsonTestMapper.class);
 
-            JsonTestRecord record1 = new JsonTestRecord();
-            record1.setId(1);
-            record1.setDescription("Fred");
-            record1.setInfo("{\"firstName\": \"Fred\", \"lastName\": \"Flintstone\", \"age\": 30}");
+            JsonTestRecord record1 = new JsonTestRecord(1, "Fred",
+                    "{\"firstName\": \"Fred\", \"lastName\": \"Flintstone\", \"age\": 30}");
 
-            JsonTestRecord record2 = new JsonTestRecord();
-            record2.setId(2);
-            record2.setDescription("Wilma");
-            record2.setInfo("{\"firstName\": \"Wilma\", \"lastName\": \"Flintstone\", \"age\": 25}");
+            JsonTestRecord record2 = new JsonTestRecord(2, "Wilma",
+                    "{\"firstName\": \"Wilma\", \"lastName\": \"Flintstone\", \"age\": 25}");
 
             MultiRowInsertStatementProvider<JsonTestRecord> insertStatement = insertMultiple(record1, record2)
                     .into(jsonTest)
@@ -316,7 +300,7 @@ void testDeReference() {
             Optional<JsonTestRecord> row = mapper.selectOne(selectStatement);
 
             assertThat(row).hasValueSatisfying( r ->
-                assertThat(r.getInfo())
+                assertThat(r.info())
                         .isEqualTo("{\"firstName\": \"Wilma\", \"lastName\": \"Flintstone\", \"age\": 25}")
             );
         }
@@ -327,15 +311,11 @@ void testDereference2() {
         try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
             JsonTestMapper mapper = sqlSession.getMapper(JsonTestMapper.class);
 
-            JsonTestRecord record1 = new JsonTestRecord();
-            record1.setId(1);
-            record1.setDescription("Fred");
-            record1.setInfo("{\"firstName\": \"Fred\", \"lastName\": \"Flintstone\", \"age\": 30}");
+            JsonTestRecord record1 = new JsonTestRecord(1, "Fred",
+                    "{\"firstName\": \"Fred\", \"lastName\": \"Flintstone\", \"age\": 30}");
 
-            JsonTestRecord record2 = new JsonTestRecord();
-            record2.setId(2);
-            record2.setDescription("Wilma");
-            record2.setInfo("{\"firstName\": \"Wilma\", \"lastName\": \"Flintstone\", \"age\": 25}");
+            JsonTestRecord record2 = new JsonTestRecord(2, "Wilma",
+                    "{\"firstName\": \"Wilma\", \"lastName\": \"Flintstone\", \"age\": 25}");
 
             MultiRowInsertStatementProvider<JsonTestRecord> insertStatement = insertMultiple(record1, record2)
                     .into(jsonTest)
diff --git a/src/test/java/examples/custom_render/JsonRenderingStrategy.java b/src/test/java/examples/custom_render/JsonRenderingStrategy.java
index 10f2d3c88..62c822e93 100644
--- a/src/test/java/examples/custom_render/JsonRenderingStrategy.java
+++ b/src/test/java/examples/custom_render/JsonRenderingStrategy.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/custom_render/JsonTestDynamicSqlSupport.java b/src/test/java/examples/custom_render/JsonTestDynamicSqlSupport.java
index b0edba9ff..cbdf85ce0 100644
--- a/src/test/java/examples/custom_render/JsonTestDynamicSqlSupport.java
+++ b/src/test/java/examples/custom_render/JsonTestDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/custom_render/JsonTestMapper.java b/src/test/java/examples/custom_render/JsonTestMapper.java
index bb3a8f6ac..4b919ea34 100644
--- a/src/test/java/examples/custom_render/JsonTestMapper.java
+++ b/src/test/java/examples/custom_render/JsonTestMapper.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,9 +18,7 @@
 import java.util.List;
 import java.util.Optional;
 
-import org.apache.ibatis.annotations.Result;
-import org.apache.ibatis.annotations.ResultMap;
-import org.apache.ibatis.annotations.Results;
+import org.apache.ibatis.annotations.Arg;
 import org.apache.ibatis.annotations.SelectProvider;
 import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
 import org.mybatis.dynamic.sql.util.SqlProviderAdapter;
@@ -32,14 +30,14 @@
 public interface JsonTestMapper extends CommonDeleteMapper, CommonInsertMapper<JsonTestRecord>, CommonSelectMapper,
         CommonUpdateMapper {
     @SelectProvider(type = SqlProviderAdapter.class, method = "select")
-    @Results(id = "JsonTestResult", value = {
-            @Result(column = "id", property = "id", id = true),
-            @Result(column = "description", property = "description"),
-            @Result(column = "info", property = "info")
-    })
+    @Arg(column = "id", javaType = int.class, id = true)
+    @Arg(column = "description", javaType = String.class)
+    @Arg(column = "info", javaType = String.class)
     List<JsonTestRecord> selectMany(SelectStatementProvider selectStatement);
 
     @SelectProvider(type = SqlProviderAdapter.class, method = "select")
-    @ResultMap("JsonTestResult")
+    @Arg(column = "id", javaType = int.class, id = true)
+    @Arg(column = "description", javaType = String.class)
+    @Arg(column = "info", javaType = String.class)
     Optional<JsonTestRecord> selectOne(SelectStatementProvider selectStatement);
 }
diff --git a/src/test/java/examples/custom_render/JsonTestRecord.java b/src/test/java/examples/custom_render/JsonTestRecord.java
index c12d888d4..6b7090d58 100644
--- a/src/test/java/examples/custom_render/JsonTestRecord.java
+++ b/src/test/java/examples/custom_render/JsonTestRecord.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,32 +15,4 @@
  */
 package examples.custom_render;
 
-public class JsonTestRecord {
-    private int id;
-    private String description;
-    private String info;
-
-    public int getId() {
-        return id;
-    }
-
-    public void setId(int id) {
-        this.id = id;
-    }
-
-    public String getDescription() {
-        return description;
-    }
-
-    public void setDescription(String description) {
-        this.description = description;
-    }
-
-    public String getInfo() {
-        return info;
-    }
-
-    public void setInfo(String info) {
-        this.info = info;
-    }
-}
+public record JsonTestRecord (int id, String description, String info) {}
diff --git a/src/test/java/examples/custom_render/package-info.java b/src/test/java/examples/custom_render/package-info.java
new file mode 100644
index 000000000..a735b9733
--- /dev/null
+++ b/src/test/java/examples/custom_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 examples.custom_render;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/src/test/java/examples/emptywhere/EmptyWhereTest.java b/src/test/java/examples/emptywhere/EmptyWhereTest.java
index 3f6d4a86e..6d6ea5ba7 100644
--- a/src/test/java/examples/emptywhere/EmptyWhereTest.java
+++ b/src/test/java/examples/emptywhere/EmptyWhereTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -20,9 +20,10 @@
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mybatis.dynamic.sql.SqlBuilder.*;
 
-import java.util.*;
+import java.util.Optional;
 import java.util.stream.Stream;
 
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
@@ -40,66 +41,61 @@
 import org.mybatis.dynamic.sql.where.render.WhereClauseProvider;
 
 class EmptyWhereTest {
+    private static final String FIRST_NAME = "Fred";
+    private static final String LAST_NAME = "Flintstone";
 
-    static List<Variation> baseVariations() {
-        String firstName = "Fred";
-        String lastName = "Flintstone";
-
-        Variation v1 = new Variation(firstName, lastName,
+    static Stream<Variation> whereVariations() {
+        Variation v1 = new Variation(FIRST_NAME, LAST_NAME,
                 "where first_name = #{parameters.p1} or last_name = #{parameters.p2}");
 
-        Variation v2 = new Variation(null, lastName,
+        Variation v2 = new Variation(null, LAST_NAME,
                 "where last_name = #{parameters.p1}");
 
-        Variation v3 = new Variation(firstName, null,
+        Variation v3 = new Variation(FIRST_NAME, null,
                 "where first_name = #{parameters.p1}");
 
         Variation v4 = new Variation(null, null, "");
 
-        List<Variation> answer = new ArrayList<>();
-        answer.add(v1);
-        answer.add(v2);
-        answer.add(v3);
-        answer.add(v4);
-        return answer;
-    }
-
-    static Stream<Variation> whereVariations() {
-        return baseVariations().stream();
+        return Stream.of(v1, v2, v3, v4);
     }
 
     static Stream<Variation> joinWhereVariations() {
-        List<Variation> baseVariations = baseVariations();
+        Variation v1 = new Variation(FIRST_NAME, LAST_NAME,
+                "where person.first_name = #{parameters.p1} or person.last_name = #{parameters.p2}");
 
-        baseVariations.get(0).whereClause =
-                "where person.first_name = #{parameters.p1} or person.last_name = #{parameters.p2}";
-        baseVariations.get(1).whereClause = "where person.last_name = #{parameters.p1}";
-        baseVariations.get(2).whereClause = "where person.first_name = #{parameters.p1}";
+        Variation v2 = new Variation(null, LAST_NAME,
+                "where person.last_name = #{parameters.p1}");
 
-        return baseVariations.stream();
+        Variation v3 = new Variation(FIRST_NAME, null,
+                "where person.first_name = #{parameters.p1}");
+
+        Variation v4 = new Variation(null, null, "");
+
+        return Stream.of(v1, v2, v3, v4);
     }
 
     static Stream<Variation> updateWhereVariations() {
-        List<Variation> baseVariations = baseVariations();
+        Variation v1 = new Variation(FIRST_NAME, LAST_NAME,
+                "where first_name = #{parameters.p2} or last_name = #{parameters.p3}");
 
-        baseVariations.get(0).whereClause =
-                "where first_name = #{parameters.p2} or last_name = #{parameters.p3}";
-        baseVariations.get(1).whereClause ="where last_name = #{parameters.p2}";
-        baseVariations.get(2).whereClause = "where first_name = #{parameters.p2}";
+        Variation v2 = new Variation(null, LAST_NAME,
+                "where last_name = #{parameters.p2}");
 
-        return baseVariations.stream();
+        Variation v3 = new Variation(FIRST_NAME, null,
+                "where first_name = #{parameters.p2}");
+
+        Variation v4 = new Variation(null, null, "");
+
+        return Stream.of(v1, v2, v3, v4);
     }
 
     @Test
     void testDeleteThreeConditions() {
-        String fName = "Fred";
-        String lName = "Flintstone";
-
         DeleteDSL<DeleteModel>.DeleteWhereBuilder builder = deleteFrom(person)
                 .where(id, isEqualTo(3));
 
-        builder.and(firstName, isEqualTo(fName).filter(Objects::nonNull));
-        builder.and(PersonDynamicSqlSupport.lastName, isEqualTo(lName).filter(Objects::nonNull));
+        builder.and(firstName, isEqualTo(FIRST_NAME));
+        builder.and(PersonDynamicSqlSupport.lastName, isEqualTo(LAST_NAME));
 
         DeleteStatementProvider deleteStatement = builder.build().render(RenderingStrategies.MYBATIS3);
 
@@ -117,8 +113,8 @@ void testDeleteVariations(Variation variation) {
         DeleteDSL<DeleteModel>.DeleteWhereBuilder builder = deleteFrom(person)
                 .where();
 
-        builder.and(firstName, isEqualTo(variation.firstName).filter(Objects::nonNull));
-        builder.or(PersonDynamicSqlSupport.lastName, isEqualTo(variation.lastName).filter(Objects::nonNull));
+        builder.and(firstName, isEqualToWhenPresent(variation.firstName));
+        builder.or(PersonDynamicSqlSupport.lastName, isEqualToWhenPresent(variation.lastName));
         builder.configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true));
 
         DeleteStatementProvider deleteStatement = builder.build().render(RenderingStrategies.MYBATIS3);
@@ -130,15 +126,12 @@ void testDeleteVariations(Variation variation) {
 
     @Test
     void testSelectThreeConditions() {
-        String fName = "Fred";
-        String lName = "Flintstone";
-
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder = select(id, firstName, PersonDynamicSqlSupport.lastName)
                 .from(person)
                 .where(id, isEqualTo(3));
 
-        builder.and(firstName, isEqualTo(fName).filter(Objects::nonNull));
-        builder.and(PersonDynamicSqlSupport.lastName, isEqualTo(lName).filter(Objects::nonNull));
+        builder.and(firstName, isEqualTo(FIRST_NAME));
+        builder.and(PersonDynamicSqlSupport.lastName, isEqualTo(LAST_NAME));
 
         SelectStatementProvider selectStatement = builder.build().render(RenderingStrategies.MYBATIS3);
 
@@ -158,8 +151,8 @@ void testSelectVariations(Variation variation) {
                 .from(person)
                 .where();
 
-        builder.and(firstName, isEqualTo(variation.firstName).filter(Objects::nonNull));
-        builder.or(PersonDynamicSqlSupport.lastName, isEqualTo(variation.lastName).filter(Objects::nonNull));
+        builder.and(firstName, isEqualToWhenPresent(variation.firstName));
+        builder.or(PersonDynamicSqlSupport.lastName, isEqualToWhenPresent(variation.lastName));
         builder.configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true));
 
         SelectStatementProvider selectStatement = builder.build().render(RenderingStrategies.MYBATIS3);
@@ -171,15 +164,12 @@ void testSelectVariations(Variation variation) {
 
     @Test
     void testJoinThreeConditions() {
-        String fName = "Fred";
-        String lName = "Flintstone";
-
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder = select(id, firstName, PersonDynamicSqlSupport.lastName, orderDate)
-                .from(person).join(order).on(person.id, equalTo(order.personId))
+                .from(person).join(order).on(person.id, isEqualTo(order.personId))
                 .where(id, isEqualTo(3));
 
-        builder.and(firstName, isEqualTo(fName).filter(Objects::nonNull));
-        builder.and(PersonDynamicSqlSupport.lastName, isEqualTo(lName).filter(Objects::nonNull));
+        builder.and(firstName, isEqualTo(FIRST_NAME));
+        builder.and(PersonDynamicSqlSupport.lastName, isEqualTo(LAST_NAME));
 
         SelectStatementProvider selectStatement = builder.build().render(RenderingStrategies.MYBATIS3);
 
@@ -197,11 +187,11 @@ void testJoinThreeConditions() {
     @MethodSource("joinWhereVariations")
     void testJoinVariations(Variation variation) {
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder = select(id, firstName, PersonDynamicSqlSupport.lastName, orderDate)
-                .from(person).join(order).on(person.id, equalTo(order.personId))
+                .from(person).join(order).on(person.id, isEqualTo(order.personId))
                 .where();
 
-        builder.and(firstName, isEqualTo(variation.firstName).filter(Objects::nonNull));
-        builder.or(PersonDynamicSqlSupport.lastName, isEqualTo(variation.lastName).filter(Objects::nonNull));
+        builder.and(firstName, isEqualToWhenPresent(variation.firstName));
+        builder.or(PersonDynamicSqlSupport.lastName, isEqualToWhenPresent(variation.lastName));
         builder.configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true));
 
         SelectStatementProvider selectStatement = builder.build().render(RenderingStrategies.MYBATIS3);
@@ -216,15 +206,12 @@ void testJoinVariations(Variation variation) {
 
     @Test
     void testUpdateThreeConditions() {
-        String fName = "Fred";
-        String lName = "Flintstone";
-
         UpdateDSL<UpdateModel>.UpdateWhereBuilder builder = update(person)
                 .set(id).equalTo(3)
                 .where(id, isEqualTo(3));
 
-        builder.and(firstName, isEqualTo(fName).filter(Objects::nonNull));
-        builder.and(PersonDynamicSqlSupport.lastName, isEqualTo(lName).filter(Objects::nonNull));
+        builder.and(firstName, isEqualTo(FIRST_NAME));
+        builder.and(PersonDynamicSqlSupport.lastName, isEqualTo(LAST_NAME));
 
         UpdateStatementProvider updateStatement = builder.build().render(RenderingStrategies.MYBATIS3);
 
@@ -244,8 +231,8 @@ void testUpdateVariations(Variation variation) {
                 .set(id).equalTo(3)
                 .where();
 
-        builder.and(firstName, isEqualTo(variation.firstName).filter(Objects::nonNull));
-        builder.or(PersonDynamicSqlSupport.lastName, isEqualTo(variation.lastName).filter(Objects::nonNull));
+        builder.and(firstName, isEqualToWhenPresent(variation.firstName));
+        builder.or(PersonDynamicSqlSupport.lastName, isEqualToWhenPresent(variation.lastName));
         builder.configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true));
 
         UpdateStatementProvider updateStatement = builder.build().render(RenderingStrategies.MYBATIS3);
@@ -259,13 +246,10 @@ void testUpdateVariations(Variation variation) {
 
     @Test
     void testWhereThreeConditions() {
-        String fName = "Fred";
-        String lName = "Flintstone";
-
         WhereDSL.StandaloneWhereFinisher builder = where(id, isEqualTo(3));
 
-        builder.and(firstName, isEqualTo(fName).filter(Objects::nonNull));
-        builder.and(PersonDynamicSqlSupport.lastName, isEqualTo(lName).filter(Objects::nonNull));
+        builder.and(firstName, isEqualTo(FIRST_NAME));
+        builder.and(PersonDynamicSqlSupport.lastName, isEqualTo(LAST_NAME));
 
         Optional<WhereClauseProvider> whereClause = builder.build().render(RenderingStrategies.MYBATIS3);
 
@@ -283,8 +267,8 @@ void testWhereThreeConditions() {
     void testWhereVariations(Variation variation) {
         WhereDSL.StandaloneWhereFinisher builder = where();
 
-        builder.and(firstName, isEqualTo(variation.firstName).filter(Objects::nonNull));
-        builder.or(PersonDynamicSqlSupport.lastName, isEqualTo(variation.lastName).filter(Objects::nonNull));
+        builder.and(firstName, isEqualToWhenPresent(variation.firstName));
+        builder.or(PersonDynamicSqlSupport.lastName, isEqualToWhenPresent(variation.lastName));
         builder.configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true));
 
         Optional<WhereClauseProvider> whereClause = builder.build().render(RenderingStrategies.MYBATIS3);
@@ -298,15 +282,5 @@ void testWhereVariations(Variation variation) {
         }
     }
 
-    private static class Variation {
-        String firstName;
-        String lastName;
-        String whereClause;
-
-        public Variation(String firstName, String lastName, String whereClause) {
-            this.firstName = firstName;
-            this.lastName = lastName;
-            this.whereClause = whereClause;
-        }
-    }
+    private record Variation (@Nullable String firstName, @Nullable String lastName, String whereClause) {}
 }
diff --git a/src/test/java/examples/emptywhere/OrderDynamicSqlSupport.java b/src/test/java/examples/emptywhere/OrderDynamicSqlSupport.java
index 1e152ecdb..41cab1031 100644
--- a/src/test/java/examples/emptywhere/OrderDynamicSqlSupport.java
+++ b/src/test/java/examples/emptywhere/OrderDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -22,13 +22,13 @@
 
 public class OrderDynamicSqlSupport {
 
-    public static Order order = new Order();
-    public static SqlColumn<Integer> personId = order.personId;
-    public static SqlColumn<Date> orderDate = order.orderDate;
+    public static final Order order = new Order();
+    public static final SqlColumn<Integer> personId = order.personId;
+    public static final SqlColumn<Date> orderDate = order.orderDate;
 
     public static class Order extends SqlTable {
-        public SqlColumn<Integer> personId = column("person_id");
-        public SqlColumn<Date> orderDate = column("order_date");
+        public final SqlColumn<Integer> personId = column("person_id");
+        public final SqlColumn<Date> orderDate = column("order_date");
 
         public Order() {
             super("order");
diff --git a/src/test/java/examples/emptywhere/PersonDynamicSqlSupport.java b/src/test/java/examples/emptywhere/PersonDynamicSqlSupport.java
index 0719edcd0..9a7d83a81 100644
--- a/src/test/java/examples/emptywhere/PersonDynamicSqlSupport.java
+++ b/src/test/java/examples/emptywhere/PersonDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -20,15 +20,15 @@
 
 public class PersonDynamicSqlSupport {
 
-    public static Person person = new Person();
-    public static SqlColumn<Integer> id = person.id;
-    public static SqlColumn<String> firstName = person.firstName;
-    public static SqlColumn<String> lastName = person.lastName;
+    public static final Person person = new Person();
+    public static final SqlColumn<Integer> id = person.id;
+    public static final SqlColumn<String> firstName = person.firstName;
+    public static final SqlColumn<String> lastName = person.lastName;
 
     public static class Person extends SqlTable {
-        public SqlColumn<Integer> id = column("id");
-        public SqlColumn<String> firstName = column("first_name");
-        public SqlColumn<String> lastName = column("last_name");
+        public final SqlColumn<Integer> id = column("id");
+        public final SqlColumn<String> firstName = column("first_name");
+        public final SqlColumn<String> lastName = column("last_name");
 
         public Person() {
             super("person");
diff --git a/src/test/java/examples/emptywhere/package-info.java b/src/test/java/examples/emptywhere/package-info.java
new file mode 100644
index 000000000..89f32f987
--- /dev/null
+++ b/src/test/java/examples/emptywhere/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 examples.emptywhere;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/src/test/java/examples/generated/always/GeneratedAlwaysRecord.java b/src/test/java/examples/generated/always/GeneratedAlwaysRecord.java
index c539f0f10..9717893e8 100644
--- a/src/test/java/examples/generated/always/GeneratedAlwaysRecord.java
+++ b/src/test/java/examples/generated/always/GeneratedAlwaysRecord.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/generated/always/PersonRecord.java b/src/test/java/examples/generated/always/PersonRecord.java
index 01a440d87..e38c18527 100644
--- a/src/test/java/examples/generated/always/PersonRecord.java
+++ b/src/test/java/examples/generated/always/PersonRecord.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/generated/always/mybatis/GeneratedAlwaysDynamicSqlSupport.java b/src/test/java/examples/generated/always/mybatis/GeneratedAlwaysDynamicSqlSupport.java
index 57f125e7e..6a7faf8bc 100644
--- a/src/test/java/examples/generated/always/mybatis/GeneratedAlwaysDynamicSqlSupport.java
+++ b/src/test/java/examples/generated/always/mybatis/GeneratedAlwaysDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/generated/always/mybatis/GeneratedAlwaysMapper.java b/src/test/java/examples/generated/always/mybatis/GeneratedAlwaysMapper.java
index ac2f7f86b..eb18c1020 100644
--- a/src/test/java/examples/generated/always/mybatis/GeneratedAlwaysMapper.java
+++ b/src/test/java/examples/generated/always/mybatis/GeneratedAlwaysMapper.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -76,8 +76,8 @@ default List<GeneratedAlwaysRecord> select(SelectDSLCompleter completer) {
         return MyBatis3Utils.selectList(this::selectMany, selectList, generatedAlways, completer);
     }
 
-    default Optional<GeneratedAlwaysRecord> selectByPrimaryKey(Integer _id) {
-        return selectOne(c -> c.where(id, isEqualTo(_id)));
+    default Optional<GeneratedAlwaysRecord> selectByPrimaryKey(Integer recordId) {
+        return selectOne(c -> c.where(id, isEqualTo(recordId)));
     }
 
     default int insert(GeneratedAlwaysRecord row) {
diff --git a/src/test/java/examples/generated/always/mybatis/GeneratedAlwaysMapperTest.java b/src/test/java/examples/generated/always/mybatis/GeneratedAlwaysMapperTest.java
index 7313e6871..ff70701fd 100644
--- a/src/test/java/examples/generated/always/mybatis/GeneratedAlwaysMapperTest.java
+++ b/src/test/java/examples/generated/always/mybatis/GeneratedAlwaysMapperTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/generated/always/mybatis/GeneratedKey.java b/src/test/java/examples/generated/always/mybatis/GeneratedKey.java
index f97a0f37e..8a65ac7ec 100644
--- a/src/test/java/examples/generated/always/mybatis/GeneratedKey.java
+++ b/src/test/java/examples/generated/always/mybatis/GeneratedKey.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/generated/always/mybatis/GeneratedKeyList.java b/src/test/java/examples/generated/always/mybatis/GeneratedKeyList.java
index e1d196fde..d172d238d 100644
--- a/src/test/java/examples/generated/always/mybatis/GeneratedKeyList.java
+++ b/src/test/java/examples/generated/always/mybatis/GeneratedKeyList.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/generated/always/mybatis/PersonDynamicSqlSupport.java b/src/test/java/examples/generated/always/mybatis/PersonDynamicSqlSupport.java
index 53c1340f6..47a7cd3a3 100644
--- a/src/test/java/examples/generated/always/mybatis/PersonDynamicSqlSupport.java
+++ b/src/test/java/examples/generated/always/mybatis/PersonDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/generated/always/mybatis/PersonMapper.java b/src/test/java/examples/generated/always/mybatis/PersonMapper.java
index 08058d17d..05d2177de 100644
--- a/src/test/java/examples/generated/always/mybatis/PersonMapper.java
+++ b/src/test/java/examples/generated/always/mybatis/PersonMapper.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/generated/always/mybatis/PersonMapperTest.java b/src/test/java/examples/generated/always/mybatis/PersonMapperTest.java
index 5687b12ea..4be1c0fcd 100644
--- a/src/test/java/examples/generated/always/mybatis/PersonMapperTest.java
+++ b/src/test/java/examples/generated/always/mybatis/PersonMapperTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/generated/always/spring/GeneratedAlwaysDynamicSqlSupport.java b/src/test/java/examples/generated/always/spring/GeneratedAlwaysDynamicSqlSupport.java
index fa22c9f00..9a96f8bc2 100644
--- a/src/test/java/examples/generated/always/spring/GeneratedAlwaysDynamicSqlSupport.java
+++ b/src/test/java/examples/generated/always/spring/GeneratedAlwaysDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/generated/always/spring/SpringTest.java b/src/test/java/examples/generated/always/spring/SpringTest.java
index a84a31bbf..59a241368 100644
--- a/src/test/java/examples/generated/always/spring/SpringTest.java
+++ b/src/test/java/examples/generated/always/spring/SpringTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/groupby/AddressDynamicSqlSupport.java b/src/test/java/examples/groupby/AddressDynamicSqlSupport.java
index defe42d65..b20ef6f72 100644
--- a/src/test/java/examples/groupby/AddressDynamicSqlSupport.java
+++ b/src/test/java/examples/groupby/AddressDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/groupby/GroupByTest.java b/src/test/java/examples/groupby/GroupByTest.java
index 0a5ce6645..13a9a7adc 100644
--- a/src/test/java/examples/groupby/GroupByTest.java
+++ b/src/test/java/examples/groupby/GroupByTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -126,7 +126,7 @@ void testGroupByAfterJoin() {
             CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class);
 
             SelectStatementProvider selectStatement = select(lastName, streetAddress, count().as("count"))
-                    .from(person, "p").join(address, "a").on(person.addressId, equalTo(address.id))
+                    .from(person, "p").join(address, "a").on(person.addressId, isEqualTo(address.id))
                     .groupBy(lastName, streetAddress)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -156,10 +156,10 @@ void testUnionAfterJoin() {
             CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class);
 
             SelectStatementProvider selectStatement = select(lastName, firstName, streetAddress)
-                    .from(person, "p").join(address, "a").on(person.addressId, equalTo(address.id))
+                    .from(person, "p").join(address, "a").on(person.addressId, isEqualTo(address.id))
                     .union()
                     .select(person2.lastName, person2.firstName, streetAddress)
-                    .from(person2, "p").join(address, "a").on(person2.addressId, equalTo(address.id))
+                    .from(person2, "p").join(address, "a").on(person2.addressId, isEqualTo(address.id))
                     .orderBy(lastName, firstName)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -192,10 +192,10 @@ void testUnionAllAfterJoin() {
             CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class);
 
             SelectStatementProvider selectStatement = select(lastName, firstName, streetAddress)
-                    .from(person, "p").join(address, "a").on(person.addressId, equalTo(address.id))
+                    .from(person, "p").join(address, "a").on(person.addressId, isEqualTo(address.id))
                     .unionAll()
                     .select(person2.lastName, person2.firstName, streetAddress)
-                    .from(person2, "p").join(address, "a").on(person2.addressId, equalTo(address.id))
+                    .from(person2, "p").join(address, "a").on(person2.addressId, isEqualTo(address.id))
                     .orderBy(lastName, firstName)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
diff --git a/src/test/java/examples/groupby/Person2DynamicSqlSupport.java b/src/test/java/examples/groupby/Person2DynamicSqlSupport.java
index 49d784b15..a65813b37 100644
--- a/src/test/java/examples/groupby/Person2DynamicSqlSupport.java
+++ b/src/test/java/examples/groupby/Person2DynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/groupby/PersonDynamicSqlSupport.java b/src/test/java/examples/groupby/PersonDynamicSqlSupport.java
index 679a6c2bc..9549eb6fd 100644
--- a/src/test/java/examples/groupby/PersonDynamicSqlSupport.java
+++ b/src/test/java/examples/groupby/PersonDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/joins/ExistsTest.java b/src/test/java/examples/joins/ExistsTest.java
index a742efecb..4161f5448 100644
--- a/src/test/java/examples/joins/ExistsTest.java
+++ b/src/test/java/examples/joins/ExistsTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/joins/ItemMasterDynamicSQLSupport.java b/src/test/java/examples/joins/ItemMasterDynamicSQLSupport.java
index 834330aac..15bf72505 100644
--- a/src/test/java/examples/joins/ItemMasterDynamicSQLSupport.java
+++ b/src/test/java/examples/joins/ItemMasterDynamicSQLSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/joins/JoinMapper.java b/src/test/java/examples/joins/JoinMapper.java
index 08c83ff38..66241b7e5 100644
--- a/src/test/java/examples/joins/JoinMapper.java
+++ b/src/test/java/examples/joins/JoinMapper.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,9 +17,8 @@
 
 import java.util.List;
 
-import org.apache.ibatis.annotations.Result;
+import org.apache.ibatis.annotations.Arg;
 import org.apache.ibatis.annotations.ResultMap;
-import org.apache.ibatis.annotations.Results;
 import org.apache.ibatis.annotations.SelectProvider;
 import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
 import org.mybatis.dynamic.sql.util.SqlProviderAdapter;
@@ -30,10 +29,8 @@ public interface JoinMapper {
     List<OrderMaster> selectMany(SelectStatementProvider selectStatement);
 
     @SelectProvider(type=SqlProviderAdapter.class, method="select")
-    @Results ({
-        @Result(column="user_id", property="userId"),
-        @Result(column="user_name", property="userName"),
-        @Result(column="parent_id", property="parentId")
-    })
+    @Arg(column="user_id", javaType = Integer.class)
+    @Arg(column="user_name", javaType = String.class)
+    @Arg(column="parent_id", javaType = Integer.class)
     List<User> selectUsers(SelectStatementProvider selectStatement);
 }
diff --git a/src/test/java/examples/joins/JoinMapperTest.java b/src/test/java/examples/joins/JoinMapperTest.java
index e84571231..1f1c6a27c 100644
--- a/src/test/java/examples/joins/JoinMapperTest.java
+++ b/src/test/java/examples/joins/JoinMapperTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -83,7 +83,7 @@ void testSingleTableJoin1() {
 
             SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity)
                     .from(orderMaster, "om")
-                    .join(orderDetail, "od").on(orderMaster.orderId, equalTo(orderDetail.orderId))
+                    .join(orderDetail, "od").on(orderMaster.orderId, isEqualTo(orderDetail.orderId))
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
 
@@ -98,15 +98,15 @@ void testSingleTableJoin1() {
             assertThat(orderMaster.getId()).isEqualTo(1);
             assertThat(orderMaster.getDetails()).hasSize(2);
             OrderDetail orderDetail = orderMaster.getDetails().get(0);
-            assertThat(orderDetail.getLineNumber()).isEqualTo(1);
+            assertThat(orderDetail.lineNumber()).isEqualTo(1);
             orderDetail = orderMaster.getDetails().get(1);
-            assertThat(orderDetail.getLineNumber()).isEqualTo(2);
+            assertThat(orderDetail.lineNumber()).isEqualTo(2);
 
             orderMaster = rows.get(1);
             assertThat(orderMaster.getId()).isEqualTo(2);
             assertThat(orderMaster.getDetails()).hasSize(1);
             orderDetail = orderMaster.getDetails().get(0);
-            assertThat(orderDetail.getLineNumber()).isEqualTo(1);
+            assertThat(orderDetail.lineNumber()).isEqualTo(1);
         }
     }
 
@@ -117,7 +117,7 @@ void testSingleTableJoin2() {
 
             SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity)
                     .from(orderMaster, "om")
-                    .join(orderDetail, "od", on(orderMaster.orderId, equalTo(orderDetail.orderId)))
+                    .join(orderDetail, "od", on(orderMaster.orderId, isEqualTo(orderDetail.orderId)))
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
 
@@ -132,15 +132,15 @@ void testSingleTableJoin2() {
             assertThat(orderMaster.getId()).isEqualTo(1);
             assertThat(orderMaster.getDetails()).hasSize(2);
             OrderDetail orderDetail = orderMaster.getDetails().get(0);
-            assertThat(orderDetail.getLineNumber()).isEqualTo(1);
+            assertThat(orderDetail.lineNumber()).isEqualTo(1);
             orderDetail = orderMaster.getDetails().get(1);
-            assertThat(orderDetail.getLineNumber()).isEqualTo(2);
+            assertThat(orderDetail.lineNumber()).isEqualTo(2);
 
             orderMaster = rows.get(1);
             assertThat(orderMaster.getId()).isEqualTo(2);
             assertThat(orderMaster.getDetails()).hasSize(1);
             orderDetail = orderMaster.getDetails().get(0);
-            assertThat(orderDetail.getLineNumber()).isEqualTo(1);
+            assertThat(orderDetail.lineNumber()).isEqualTo(1);
         }
     }
 
@@ -149,7 +149,7 @@ void testCompoundJoin1() {
         // this is a nonsensical join, but it does test the "and" capability
         SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity)
                 .from(orderMaster, "om")
-                .join(orderDetail, "od").on(orderMaster.orderId, equalTo(orderDetail.orderId), and(orderMaster.orderId, equalTo(orderDetail.orderId)))
+                .join(orderDetail, "od").on(orderMaster.orderId, isEqualTo(orderDetail.orderId), and(orderMaster.orderId, isEqualTo(orderDetail.orderId)))
                 .build()
                 .render(RenderingStrategies.MYBATIS3);
 
@@ -163,9 +163,9 @@ void testCompoundJoin2() {
         // this is a nonsensical join, but it does test the "and" capability
         SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity)
                 .from(orderMaster, "om")
-                .join(orderDetail, "od").on(orderMaster.orderId, equalTo(orderDetail.orderId))
+                .join(orderDetail, "od").on(orderMaster.orderId, isEqualTo(orderDetail.orderId))
                 .configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true))
-                .and(orderMaster.orderId, equalTo(orderDetail.orderId))
+                .and(orderMaster.orderId, isEqualTo(orderDetail.orderId))
                 .build()
                 .render(RenderingStrategies.MYBATIS3);
 
@@ -179,7 +179,7 @@ void testCompoundJoin3() {
         // this is a nonsensical join, but it does test the "and" capability
         SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity)
                 .from(orderMaster, "om")
-                .join(orderDetail, "od", on(orderMaster.orderId, equalTo(orderDetail.orderId)), and(orderMaster.orderId, equalTo(orderDetail.orderId)))
+                .join(orderDetail, "od", on(orderMaster.orderId, isEqualTo(orderDetail.orderId)), and(orderMaster.orderId, isEqualTo(orderDetail.orderId)))
                 .build()
                 .render(RenderingStrategies.MYBATIS3);
 
@@ -193,7 +193,7 @@ void testCompoundJoin4() {
         // this is a nonsensical join, but it does test the "and" capability
         SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity)
                 .from(orderMaster, "om")
-                .leftJoin(orderDetail, "od", on(orderMaster.orderId, equalTo(orderDetail.orderId)), and(orderMaster.orderId, equalTo(orderDetail.orderId)))
+                .leftJoin(orderDetail, "od", on(orderMaster.orderId, isEqualTo(orderDetail.orderId)), and(orderMaster.orderId, isEqualTo(orderDetail.orderId)))
                 .build()
                 .render(RenderingStrategies.MYBATIS3);
 
@@ -207,7 +207,7 @@ void testCompoundJoin5() {
         // this is a nonsensical join, but it does test the "and" capability
         SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity)
                 .from(orderMaster, "om")
-                .rightJoin(orderDetail, "od", on(orderMaster.orderId, equalTo(orderDetail.orderId)), and(orderMaster.orderId, equalTo(orderDetail.orderId)))
+                .rightJoin(orderDetail, "od", on(orderMaster.orderId, isEqualTo(orderDetail.orderId)), and(orderMaster.orderId, isEqualTo(orderDetail.orderId)))
                 .build()
                 .render(RenderingStrategies.MYBATIS3);
 
@@ -221,7 +221,7 @@ void testCompoundJoin6() {
         // this is a nonsensical join, but it does test the "and" capability
         SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity)
                 .from(orderMaster, "om")
-                .fullJoin(orderDetail, "od", on(orderMaster.orderId, equalTo(orderDetail.orderId)), and(orderMaster.orderId, equalTo(orderDetail.orderId)))
+                .fullJoin(orderDetail, "od", on(orderMaster.orderId, isEqualTo(orderDetail.orderId)), and(orderMaster.orderId, isEqualTo(orderDetail.orderId)))
                 .build()
                 .render(RenderingStrategies.MYBATIS3);
 
@@ -237,8 +237,8 @@ void testMultipleTableJoinWithWhereClause() {
 
             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))
+                    .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);
@@ -255,9 +255,9 @@ void testMultipleTableJoinWithWhereClause() {
             assertThat(orderMaster.getId()).isEqualTo(2);
             assertThat(orderMaster.getDetails()).hasSize(2);
             OrderDetail orderDetail = orderMaster.getDetails().get(0);
-            assertThat(orderDetail.getLineNumber()).isEqualTo(1);
+            assertThat(orderDetail.lineNumber()).isEqualTo(1);
             orderDetail = orderMaster.getDetails().get(1);
-            assertThat(orderDetail.getLineNumber()).isEqualTo(2);
+            assertThat(orderDetail.lineNumber()).isEqualTo(2);
         }
     }
 
@@ -268,8 +268,8 @@ void testMultipleTableJoinWithApplyWhere() {
 
             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))
+                    .join(orderLine, "ol").on(orderMaster.orderId, isEqualTo(orderLine.orderId))
+                    .join(itemMaster, "im").on(orderLine.itemId, isEqualTo(itemMaster.itemId))
                     .applyWhere(where(orderMaster.orderId, isEqualTo(2)).toWhereApplier())
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -286,9 +286,9 @@ void testMultipleTableJoinWithApplyWhere() {
             assertThat(orderMaster.getId()).isEqualTo(2);
             assertThat(orderMaster.getDetails()).hasSize(2);
             OrderDetail orderDetail = orderMaster.getDetails().get(0);
-            assertThat(orderDetail.getLineNumber()).isEqualTo(1);
+            assertThat(orderDetail.lineNumber()).isEqualTo(1);
             orderDetail = orderMaster.getDetails().get(1);
-            assertThat(orderDetail.getLineNumber()).isEqualTo(2);
+            assertThat(orderDetail.lineNumber()).isEqualTo(2);
         }
     }
 
@@ -299,8 +299,8 @@ void testMultipleTableJoinWithComplexWhereClause() {
 
             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))
+                    .join(orderLine, "ol").on(orderMaster.orderId, isEqualTo(orderLine.orderId))
+                    .join(itemMaster, "im").on(orderLine.itemId, isEqualTo(itemMaster.itemId))
                     .where(orderMaster.orderId, isEqualTo(2), and(orderLine.lineNumber, isEqualTo(2)))
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -317,7 +317,7 @@ void testMultipleTableJoinWithComplexWhereClause() {
             assertThat(orderMaster.getId()).isEqualTo(2);
             assertThat(orderMaster.getDetails()).hasSize(1);
             OrderDetail orderDetail = orderMaster.getDetails().get(0);
-            assertThat(orderDetail.getLineNumber()).isEqualTo(2);
+            assertThat(orderDetail.lineNumber()).isEqualTo(2);
         }
     }
 
@@ -328,8 +328,8 @@ void testMultipleTableJoinWithOrderBy() {
 
             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))
+                    .join(orderLine, "ol").on(orderMaster.orderId, isEqualTo(orderLine.orderId))
+                    .join(itemMaster, "im").on(orderLine.itemId, isEqualTo(itemMaster.itemId))
                     .orderBy(orderMaster.orderId)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -346,15 +346,15 @@ void testMultipleTableJoinWithOrderBy() {
             assertThat(orderMaster.getId()).isEqualTo(1);
             assertThat(orderMaster.getDetails()).hasSize(1);
             OrderDetail orderDetail = orderMaster.getDetails().get(0);
-            assertThat(orderDetail.getLineNumber()).isEqualTo(1);
+            assertThat(orderDetail.lineNumber()).isEqualTo(1);
 
             orderMaster = rows.get(1);
             assertThat(orderMaster.getId()).isEqualTo(2);
             assertThat(orderMaster.getDetails()).hasSize(2);
             orderDetail = orderMaster.getDetails().get(0);
-            assertThat(orderDetail.getLineNumber()).isEqualTo(1);
+            assertThat(orderDetail.lineNumber()).isEqualTo(1);
             orderDetail = orderMaster.getDetails().get(1);
-            assertThat(orderDetail.getLineNumber()).isEqualTo(2);
+            assertThat(orderDetail.lineNumber()).isEqualTo(2);
         }
     }
 
@@ -365,8 +365,8 @@ void testMultipleTableJoinNoAliasWithOrderBy() {
 
             SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderLine.lineNumber, itemMaster.description, orderLine.quantity)
                     .from(orderMaster)
-                    .join(orderLine).on(orderMaster.orderId, equalTo(orderLine.orderId))
-                    .join(itemMaster).on(orderLine.itemId, equalTo(itemMaster.itemId))
+                    .join(orderLine).on(orderMaster.orderId, isEqualTo(orderLine.orderId))
+                    .join(itemMaster).on(orderLine.itemId, isEqualTo(itemMaster.itemId))
                     .where(orderMaster.orderId, isEqualTo(2))
                     .orderBy(orderMaster.orderId)
                     .build()
@@ -385,9 +385,9 @@ void testMultipleTableJoinNoAliasWithOrderBy() {
             assertThat(orderMaster.getId()).isEqualTo(2);
             assertThat(orderMaster.getDetails()).hasSize(2);
             OrderDetail orderDetail = orderMaster.getDetails().get(0);
-            assertThat(orderDetail.getLineNumber()).isEqualTo(1);
+            assertThat(orderDetail.lineNumber()).isEqualTo(1);
             orderDetail = orderMaster.getDetails().get(1);
-            assertThat(orderDetail.getLineNumber()).isEqualTo(2);
+            assertThat(orderDetail.lineNumber()).isEqualTo(2);
         }
     }
 
@@ -398,7 +398,7 @@ void testRightJoin() {
 
             SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description)
                     .from(orderLine, "ol")
-                    .rightJoin(itemMaster, "im").on(orderLine.itemId, equalTo(itemMaster.itemId))
+                    .rightJoin(itemMaster, "im").on(orderLine.itemId, isEqualTo(itemMaster.itemId))
                     .orderBy(itemMaster.itemId)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -432,8 +432,8 @@ void testRightJoin2() {
 
             SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description)
                     .from(orderMaster, "om")
-                    .join(orderLine, "ol").on(orderMaster.orderId, equalTo(orderLine.orderId))
-                    .rightJoin(itemMaster, "im").on(orderLine.itemId, equalTo(itemMaster.itemId))
+                    .join(orderLine, "ol").on(orderMaster.orderId, isEqualTo(orderLine.orderId))
+                    .rightJoin(itemMaster, "im").on(orderLine.itemId, isEqualTo(itemMaster.itemId))
                     .orderBy(orderLine.orderId, itemMaster.itemId)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -468,8 +468,8 @@ void testRightJoin3() {
 
             SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description)
                     .from(orderMaster, "om")
-                    .join(orderLine, "ol", on(orderMaster.orderId, equalTo(orderLine.orderId)))
-                    .rightJoin(itemMaster, "im", on(orderLine.itemId, equalTo(itemMaster.itemId)))
+                    .join(orderLine, "ol", on(orderMaster.orderId, isEqualTo(orderLine.orderId)))
+                    .rightJoin(itemMaster, "im", on(orderLine.itemId, isEqualTo(itemMaster.itemId)))
                     .orderBy(orderLine.orderId, itemMaster.itemId)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -504,8 +504,8 @@ void testRightJoinNoAliases() {
 
             SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description)
                     .from(orderMaster)
-                    .join(orderLine).on(orderMaster.orderId, equalTo(orderLine.orderId))
-                    .rightJoin(itemMaster).on(orderLine.itemId, equalTo(itemMaster.itemId))
+                    .join(orderLine).on(orderMaster.orderId, isEqualTo(orderLine.orderId))
+                    .rightJoin(itemMaster).on(orderLine.itemId, isEqualTo(itemMaster.itemId))
                     .orderBy(orderLine.orderId, itemMaster.itemId)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -540,7 +540,7 @@ void testLeftJoin() {
 
             SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description)
                     .from(itemMaster, "im")
-                    .leftJoin(orderLine, "ol").on(orderLine.itemId, equalTo(itemMaster.itemId))
+                    .leftJoin(orderLine, "ol").on(orderLine.itemId, isEqualTo(itemMaster.itemId))
                     .orderBy(itemMaster.itemId)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -574,8 +574,8 @@ void testLeftJoin2() {
 
             SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description)
                     .from(orderMaster, "om")
-                    .join(orderLine, "ol").on(orderMaster.orderId, equalTo(orderLine.orderId))
-                    .leftJoin(itemMaster, "im").on(orderLine.itemId, equalTo(itemMaster.itemId))
+                    .join(orderLine, "ol").on(orderMaster.orderId, isEqualTo(orderLine.orderId))
+                    .leftJoin(itemMaster, "im").on(orderLine.itemId, isEqualTo(itemMaster.itemId))
                     .orderBy(orderLine.orderId, itemMaster.itemId)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -610,8 +610,8 @@ void testLeftJoin3() {
 
             SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description)
                     .from(orderMaster, "om")
-                    .join(orderLine, "ol", on(orderMaster.orderId, equalTo(orderLine.orderId)))
-                    .leftJoin(itemMaster, "im", on(orderLine.itemId, equalTo(itemMaster.itemId)))
+                    .join(orderLine, "ol", on(orderMaster.orderId, isEqualTo(orderLine.orderId)))
+                    .leftJoin(itemMaster, "im", on(orderLine.itemId, isEqualTo(itemMaster.itemId)))
                     .orderBy(orderLine.orderId, itemMaster.itemId)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -646,8 +646,8 @@ void testLeftJoinNoAliases() {
 
             SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description)
                     .from(orderMaster)
-                    .join(orderLine).on(orderMaster.orderId, equalTo(orderLine.orderId))
-                    .leftJoin(itemMaster).on(orderLine.itemId, equalTo(itemMaster.itemId))
+                    .join(orderLine).on(orderMaster.orderId, isEqualTo(orderLine.orderId))
+                    .leftJoin(itemMaster).on(orderLine.itemId, isEqualTo(itemMaster.itemId))
                     .orderBy(orderLine.orderId, itemMaster.itemId)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -682,7 +682,7 @@ void testFullJoin() {
 
             SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, orderLine.itemId.as("ol_itemid"), itemMaster.itemId.as("im_itemid"), itemMaster.description)
                     .from(itemMaster, "im")
-                    .fullJoin(orderLine, "ol").on(itemMaster.itemId, equalTo(orderLine.itemId))
+                    .fullJoin(orderLine, "ol").on(itemMaster.itemId, isEqualTo(orderLine.itemId))
                     .orderBy(orderLine.orderId, sortColumn("im_itemid"))
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -723,8 +723,8 @@ void testFullJoin2() {
 
             SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description)
                     .from(orderMaster, "om")
-                    .join(orderLine, "ol").on(orderMaster.orderId, equalTo(orderLine.orderId))
-                    .fullJoin(itemMaster, "im").on(orderLine.itemId, equalTo(itemMaster.itemId))
+                    .join(orderLine, "ol").on(orderMaster.orderId, isEqualTo(orderLine.orderId))
+                    .fullJoin(itemMaster, "im").on(orderLine.itemId, isEqualTo(itemMaster.itemId))
                     .orderBy(orderLine.orderId, itemMaster.itemId)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -765,8 +765,8 @@ void testFullJoin3() {
 
             SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description)
                     .from(orderMaster, "om")
-                    .join(orderLine, "ol", on(orderMaster.orderId, equalTo(orderLine.orderId)))
-                    .fullJoin(itemMaster, "im", on(orderLine.itemId, equalTo(itemMaster.itemId)))
+                    .join(orderLine, "ol", on(orderMaster.orderId, isEqualTo(orderLine.orderId)))
+                    .fullJoin(itemMaster, "im", on(orderLine.itemId, isEqualTo(itemMaster.itemId)))
                     .orderBy(orderLine.orderId, itemMaster.itemId)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -807,8 +807,8 @@ void testFullJoin4() {
 
             SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.description)
                     .from(orderMaster, "om")
-                    .join(orderLine, "ol", on(orderMaster.orderId, equalTo(orderLine.orderId)))
-                    .fullJoin(itemMaster, "im", on(orderLine.itemId, equalTo(itemMaster.itemId)))
+                    .join(orderLine, "ol", on(orderMaster.orderId, isEqualTo(orderLine.orderId)))
+                    .fullJoin(itemMaster, "im", on(orderLine.itemId, isEqualTo(itemMaster.itemId)))
                     .orderBy(orderLine.orderId, sortColumn("im", itemMaster.itemId))
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -846,8 +846,8 @@ void testFullJoin5() {
 
             SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.description)
                     .from(orderMaster, "om")
-                    .join(orderLine, "ol", on(orderMaster.orderId, equalTo(orderLine.orderId)))
-                    .fullJoin(itemMaster, "im", on(orderLine.itemId, equalTo(itemMaster.itemId)))
+                    .join(orderLine, "ol", on(orderMaster.orderId, isEqualTo(orderLine.orderId)))
+                    .fullJoin(itemMaster, "im", on(orderLine.itemId, isEqualTo(itemMaster.itemId)))
                     .orderBy(orderLine.orderId, sortColumn("im", itemMaster.itemId).descending())
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -885,8 +885,8 @@ void testFullJoinNoAliases() {
 
             SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description)
                     .from(orderMaster)
-                    .join(orderLine).on(orderMaster.orderId, equalTo(orderLine.orderId))
-                    .fullJoin(itemMaster).on(orderLine.itemId, equalTo(itemMaster.itemId))
+                    .join(orderLine).on(orderMaster.orderId, isEqualTo(orderLine.orderId))
+                    .fullJoin(itemMaster).on(orderLine.itemId, isEqualTo(itemMaster.itemId))
                     .orderBy(orderLine.orderId, itemMaster.itemId)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -931,7 +931,7 @@ void testSelf() {
             // get Bamm Bamm's parent - should be Barney
             SelectStatementProvider selectStatement = select(user.userId, user.userName, user.parentId)
                     .from(user, "u1")
-                    .join(user2, "u2").on(user.userId, equalTo(user2.parentId))
+                    .join(user2, "u2").on(user.userId, isEqualTo(user2.parentId))
                     .where(user2.userId, isEqualTo(4))
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -945,9 +945,9 @@ void testSelf() {
 
             assertThat(rows).hasSize(1);
             User row = rows.get(0);
-            assertThat(row.getUserId()).isEqualTo(2);
-            assertThat(row.getUserName()).isEqualTo("Barney");
-            assertThat(row.getParentId()).isNull();
+            assertThat(row.userId()).isEqualTo(2);
+            assertThat(row.userName()).isEqualTo("Barney");
+            assertThat(row.parentId()).isNull();
         }
     }
 
@@ -957,7 +957,7 @@ void testSelfWithDuplicateAlias() {
                 .from(user, "u1");
 
         assertThatExceptionOfType(DuplicateTableAliasException.class).isThrownBy(() -> dsl.join(user, "u2"))
-                .withMessage(Messages.getString("ERROR.1", user.tableNameAtRuntime(), "u2", "u1"));
+                .withMessage(Messages.getString("ERROR.1", user.tableName(), "u2", "u1"));
     }
 
     @Test
@@ -971,7 +971,7 @@ void testSelfWithNewAlias() {
             // get Bamm Bamm's parent - should be Barney
             SelectStatementProvider selectStatement = select(user.userId, user.userName, user.parentId)
                     .from(user)
-                    .join(user2).on(user.userId, equalTo(user2.parentId))
+                    .join(user2).on(user.userId, isEqualTo(user2.parentId))
                     .where(user2.userId, isEqualTo(4))
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -985,9 +985,9 @@ void testSelfWithNewAlias() {
 
             assertThat(rows).hasSize(1);
             User row = rows.get(0);
-            assertThat(row.getUserId()).isEqualTo(2);
-            assertThat(row.getUserName()).isEqualTo("Barney");
-            assertThat(row.getParentId()).isNull();
+            assertThat(row.userId()).isEqualTo(2);
+            assertThat(row.userName()).isEqualTo("Barney");
+            assertThat(row.parentId()).isNull();
         }
     }
 
@@ -1002,7 +1002,7 @@ void testSelfWithNewAliasAndOverride() {
             // get Bamm Bamm's parent - should be Barney
             SelectStatementProvider selectStatement = select(user.userId, user.userName, user.parentId)
                     .from(user, "u1")
-                    .join(user2, "u2").on(user.userId, equalTo(user2.parentId))
+                    .join(user2, "u2").on(user.userId, isEqualTo(user2.parentId))
                     .where(user2.userId, isEqualTo(4))
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -1016,9 +1016,9 @@ void testSelfWithNewAliasAndOverride() {
 
             assertThat(rows).hasSize(1);
             User row = rows.get(0);
-            assertThat(row.getUserId()).isEqualTo(2);
-            assertThat(row.getUserName()).isEqualTo("Barney");
-            assertThat(row.getParentId()).isNull();
+            assertThat(row.userId()).isEqualTo(2);
+            assertThat(row.userName()).isEqualTo("Barney");
+            assertThat(row.parentId()).isNull();
         }
     }
 
@@ -1029,7 +1029,7 @@ void testLimitAndOffsetAfterJoin() {
 
             SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description)
                     .from(itemMaster, "im")
-                    .leftJoin(orderLine, "ol").on(orderLine.itemId, equalTo(itemMaster.itemId))
+                    .leftJoin(orderLine, "ol").on(orderLine.itemId, isEqualTo(itemMaster.itemId))
                     .limit(2)
                     .offset(1)
                     .build()
@@ -1064,7 +1064,7 @@ void testLimitOnlyAfterJoin() {
 
             SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description)
                     .from(itemMaster, "im")
-                    .leftJoin(orderLine, "ol").on(orderLine.itemId, equalTo(itemMaster.itemId))
+                    .leftJoin(orderLine, "ol").on(orderLine.itemId, isEqualTo(itemMaster.itemId))
                     .limit(2)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -1098,7 +1098,7 @@ void testOffsetOnlyAfterJoin() {
 
             SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description)
                     .from(itemMaster, "im")
-                    .leftJoin(orderLine, "ol").on(orderLine.itemId, equalTo(itemMaster.itemId))
+                    .leftJoin(orderLine, "ol").on(orderLine.itemId, isEqualTo(itemMaster.itemId))
                     .offset(2)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -1132,7 +1132,7 @@ void testOffsetAndFetchFirstAfterJoin() {
 
             SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description)
                     .from(itemMaster, "im")
-                    .leftJoin(orderLine, "ol").on(orderLine.itemId, equalTo(itemMaster.itemId))
+                    .leftJoin(orderLine, "ol").on(orderLine.itemId, isEqualTo(itemMaster.itemId))
                     .offset(1)
                     .fetchFirst(2).rowsOnly()
                     .build()
@@ -1167,7 +1167,7 @@ void testFetchFirstOnlyAfterJoin() {
 
             SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description)
                     .from(itemMaster, "im")
-                    .leftJoin(orderLine, "ol").on(orderLine.itemId, equalTo(itemMaster.itemId))
+                    .leftJoin(orderLine, "ol").on(orderLine.itemId, isEqualTo(itemMaster.itemId))
                     .fetchFirst(2).rowsOnly()
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -1201,8 +1201,8 @@ void testJoinWithParameterValue() {
 
             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))
+                    .join(orderLine, "ol").on(orderLine.itemId, isEqualTo(itemMaster.itemId))
+                    .and(orderLine.orderId, isEqualTo(1))
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
 
@@ -1235,8 +1235,8 @@ void testJoinWithConstant() {
 
             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(constant("1")))
+                    .join(orderLine, "ol").on(orderLine.itemId, isEqualTo(itemMaster.itemId))
+                    .and(orderLine.orderId, isEqualTo(constant("1")))
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
 
diff --git a/src/test/java/examples/joins/JoinSubQueryTest.java b/src/test/java/examples/joins/JoinSubQueryTest.java
index 588844828..abf3eaf6f 100644
--- a/src/test/java/examples/joins/JoinSubQueryTest.java
+++ b/src/test/java/examples/joins/JoinSubQueryTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -20,7 +20,6 @@
 import static examples.joins.OrderLineDynamicSQLSupport.orderLine;
 import static examples.joins.OrderMasterDynamicSQLSupport.orderMaster;
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.mybatis.dynamic.sql.SqlBuilder.equalTo;
 import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo;
 import static org.mybatis.dynamic.sql.SqlBuilder.select;
 import static org.mybatis.dynamic.sql.SqlBuilder.sortColumn;
@@ -82,7 +81,7 @@ void testSingleTableJoin1() {
                     .from(orderMaster, "om")
                     .join(select(orderDetail.orderId, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity)
                           .from(orderDetail),
-                          "od").on(orderMaster.orderId, equalTo(orderDetail.orderId.qualifiedWith("od")))
+                          "od").on(orderMaster.orderId, isEqualTo(orderDetail.orderId.qualifiedWith("od")))
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
 
@@ -98,15 +97,15 @@ void testSingleTableJoin1() {
             assertThat(orderMaster.getId()).isEqualTo(1);
             assertThat(orderMaster.getDetails()).hasSize(2);
             OrderDetail orderDetail = orderMaster.getDetails().get(0);
-            assertThat(orderDetail.getLineNumber()).isEqualTo(1);
+            assertThat(orderDetail.lineNumber()).isEqualTo(1);
             orderDetail = orderMaster.getDetails().get(1);
-            assertThat(orderDetail.getLineNumber()).isEqualTo(2);
+            assertThat(orderDetail.lineNumber()).isEqualTo(2);
 
             orderMaster = rows.get(1);
             assertThat(orderMaster.getId()).isEqualTo(2);
             assertThat(orderMaster.getDetails()).hasSize(1);
             orderDetail = orderMaster.getDetails().get(0);
-            assertThat(orderDetail.getLineNumber()).isEqualTo(1);
+            assertThat(orderDetail.lineNumber()).isEqualTo(1);
         }
     }
 
@@ -121,11 +120,11 @@ void testMultipleTableJoinWithWhereClause() {
                     .join(select(orderLine.orderId, orderLine.itemId, orderLine.quantity, orderLine.lineNumber)
                             .from(orderLine),
                             "ol")
-                    .on(orderMaster.orderId, equalTo(orderLine.orderId.qualifiedWith("ol")))
+                    .on(orderMaster.orderId, isEqualTo(orderLine.orderId.qualifiedWith("ol")))
                     .join(select(itemMaster.itemId, itemMaster.description)
                             .from(itemMaster),
                             "im")
-                    .on(orderLine.itemId.qualifiedWith("ol"), equalTo(itemMaster.itemId.qualifiedWith("im")))
+                    .on(orderLine.itemId.qualifiedWith("ol"), isEqualTo(itemMaster.itemId.qualifiedWith("im")))
                     .where(orderMaster.orderId, isEqualTo(2))
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -144,9 +143,9 @@ void testMultipleTableJoinWithWhereClause() {
             assertThat(orderMaster.getId()).isEqualTo(2);
             assertThat(orderMaster.getDetails()).hasSize(2);
             OrderDetail orderDetail = orderMaster.getDetails().get(0);
-            assertThat(orderDetail.getLineNumber()).isEqualTo(1);
+            assertThat(orderDetail.lineNumber()).isEqualTo(1);
             orderDetail = orderMaster.getDetails().get(1);
-            assertThat(orderDetail.getLineNumber()).isEqualTo(2);
+            assertThat(orderDetail.lineNumber()).isEqualTo(2);
         }
     }
 
@@ -158,9 +157,9 @@ void testMultipleTableJoinWithSelectStar() {
             SelectStatementProvider selectStatement = select(orderMaster.orderId, orderMaster.orderDate, orderLine.lineNumber, itemMaster.description, orderLine.quantity)
                     .from(orderMaster, "om")
                     .join(select(orderLine.allColumns()).from(orderLine), "ol")
-                    .on(orderMaster.orderId, equalTo(orderLine.orderId.qualifiedWith("ol")))
+                    .on(orderMaster.orderId, isEqualTo(orderLine.orderId.qualifiedWith("ol")))
                     .join(select(itemMaster.allColumns()).from(itemMaster), "im")
-                    .on(orderLine.itemId.qualifiedWith("ol"), equalTo(itemMaster.itemId.qualifiedWith("im")))
+                    .on(orderLine.itemId.qualifiedWith("ol"), isEqualTo(itemMaster.itemId.qualifiedWith("im")))
                     .where(orderMaster.orderId, isEqualTo(2))
                     .orderBy(orderMaster.orderId)
                     .build()
@@ -181,10 +180,10 @@ void testMultipleTableJoinWithSelectStar() {
             assertThat(orderMaster.getDetails()).hasSize(2);
 
             OrderDetail orderDetail = orderMaster.getDetails().get(0);
-            assertThat(orderDetail.getLineNumber()).isEqualTo(1);
+            assertThat(orderDetail.lineNumber()).isEqualTo(1);
 
             orderDetail = orderMaster.getDetails().get(1);
-            assertThat(orderDetail.getLineNumber()).isEqualTo(2);
+            assertThat(orderDetail.lineNumber()).isEqualTo(2);
         }
     }
 
@@ -197,7 +196,7 @@ void testRightJoin() {
                     itemMaster.itemId.qualifiedWith("im"), itemMaster.description)
                     .from(orderLine, "ol")
                     .rightJoin(select(itemMaster.allColumns()).from(itemMaster), "im")
-                    .on(orderLine.itemId, equalTo(itemMaster.itemId.qualifiedWith("im")))
+                    .on(orderLine.itemId, isEqualTo(itemMaster.itemId.qualifiedWith("im")))
                     .orderBy(itemMaster.itemId)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -232,9 +231,9 @@ void testRightJoin2() {
             SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity,
                     itemMaster.itemId.qualifiedWith(("im")), itemMaster.description)
                     .from(orderMaster, "om")
-                    .join(orderLine, "ol").on(orderMaster.orderId, equalTo(orderLine.orderId))
+                    .join(orderLine, "ol").on(orderMaster.orderId, isEqualTo(orderLine.orderId))
                     .rightJoin(select(itemMaster.allColumns()).from(itemMaster), "im")
-                    .on(orderLine.itemId, equalTo(itemMaster.itemId.qualifiedWith("im")))
+                    .on(orderLine.itemId, isEqualTo(itemMaster.itemId.qualifiedWith("im")))
                     .orderBy(orderLine.orderId, itemMaster.itemId)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -271,7 +270,7 @@ void testLeftJoin() {
                     itemMaster.itemId.qualifiedWith("im"), itemMaster.description)
                     .from(itemMaster, "im")
                     .leftJoin(select(orderLine.allColumns()).from(orderLine), "ol")
-                    .on(orderLine.itemId.qualifiedWith("ol"), equalTo(itemMaster.itemId))
+                    .on(orderLine.itemId.qualifiedWith("ol"), isEqualTo(itemMaster.itemId))
                     .orderBy(itemMaster.itemId)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -307,9 +306,9 @@ void testLeftJoin2() {
             SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity,
                     itemMaster.itemId.qualifiedWith("im"), itemMaster.description)
                     .from(orderMaster, "om")
-                    .join(orderLine, "ol").on(orderMaster.orderId, equalTo(orderLine.orderId))
+                    .join(orderLine, "ol").on(orderMaster.orderId, isEqualTo(orderLine.orderId))
                     .leftJoin(select(itemMaster.allColumns()).from(itemMaster), "im")
-                    .on(orderLine.itemId, equalTo(itemMaster.itemId.qualifiedWith("im")))
+                    .on(orderLine.itemId, isEqualTo(itemMaster.itemId.qualifiedWith("im")))
                     .orderBy(orderLine.orderId, itemMaster.itemId)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -346,7 +345,7 @@ void testFullJoin() {
                     orderLine.itemId.as("ol_itemid").qualifiedWith("ol"), itemMaster.itemId.as("im_itemid"), itemMaster.description)
                     .from(itemMaster, "im")
                     .fullJoin(select(orderLine.allColumns()).from(orderLine), "ol")
-                    .on(itemMaster.itemId, equalTo(orderLine.itemId.qualifiedWith("ol")))
+                    .on(itemMaster.itemId, isEqualTo(orderLine.itemId.qualifiedWith("ol")))
                     .orderBy(orderLine.orderId, sortColumn("im_itemid"))
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
@@ -389,9 +388,9 @@ void testFullJoin2() {
             SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity,
                     itemMaster.itemId.qualifiedWith("im"), itemMaster.description)
                     .from(orderMaster, "om")
-                    .join(orderLine, "ol").on(orderMaster.orderId, equalTo(orderLine.orderId))
+                    .join(orderLine, "ol").on(orderMaster.orderId, isEqualTo(orderLine.orderId))
                     .fullJoin(select(itemMaster.allColumns()).from(itemMaster), "im")
-                    .on(orderLine.itemId, equalTo(itemMaster.itemId.qualifiedWith("im")))
+                    .on(orderLine.itemId, isEqualTo(itemMaster.itemId.qualifiedWith("im")))
                     .orderBy(orderLine.orderId, itemMaster.itemId)
                     .build()
                     .render(RenderingStrategies.MYBATIS3);
diff --git a/src/test/java/examples/joins/OrderDetail.java b/src/test/java/examples/joins/OrderDetail.java
index 171619444..9f9abd4ce 100644
--- a/src/test/java/examples/joins/OrderDetail.java
+++ b/src/test/java/examples/joins/OrderDetail.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,41 +15,4 @@
  */
 package examples.joins;
 
-public class OrderDetail {
-    private Integer orderId;
-    private Integer lineNumber;
-    private String description;
-    private Integer quantity;
-
-    public Integer getOrderId() {
-        return orderId;
-    }
-
-    public void setOrderId(Integer orderId) {
-        this.orderId = orderId;
-    }
-
-    public Integer getLineNumber() {
-        return lineNumber;
-    }
-
-    public void setLineNumber(Integer lineNumber) {
-        this.lineNumber = lineNumber;
-    }
-
-    public String getDescription() {
-        return description;
-    }
-
-    public void setDescription(String description) {
-        this.description = description;
-    }
-
-    public Integer getQuantity() {
-        return quantity;
-    }
-
-    public void setQuantity(Integer quantity) {
-        this.quantity = quantity;
-    }
-}
+public record OrderDetail (Integer orderId, Integer lineNumber, String description, Integer quantity) {}
diff --git a/src/test/java/examples/joins/OrderDetailDynamicSQLSupport.java b/src/test/java/examples/joins/OrderDetailDynamicSQLSupport.java
index fe72cc8ac..22a391acf 100644
--- a/src/test/java/examples/joins/OrderDetailDynamicSQLSupport.java
+++ b/src/test/java/examples/joins/OrderDetailDynamicSQLSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/joins/OrderLineDynamicSQLSupport.java b/src/test/java/examples/joins/OrderLineDynamicSQLSupport.java
index d79d2c799..0ed3e749b 100644
--- a/src/test/java/examples/joins/OrderLineDynamicSQLSupport.java
+++ b/src/test/java/examples/joins/OrderLineDynamicSQLSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/joins/OrderMaster.java b/src/test/java/examples/joins/OrderMaster.java
index 72ada7ada..9972b6db9 100644
--- a/src/test/java/examples/joins/OrderMaster.java
+++ b/src/test/java/examples/joins/OrderMaster.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/joins/OrderMasterDynamicSQLSupport.java b/src/test/java/examples/joins/OrderMasterDynamicSQLSupport.java
index e92d8d566..b286971e1 100644
--- a/src/test/java/examples/joins/OrderMasterDynamicSQLSupport.java
+++ b/src/test/java/examples/joins/OrderMasterDynamicSQLSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/joins/User.java b/src/test/java/examples/joins/User.java
index ba1cb38fc..1d9e36795 100644
--- a/src/test/java/examples/joins/User.java
+++ b/src/test/java/examples/joins/User.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,32 +15,6 @@
  */
 package examples.joins;
 
-public class User {
-    private Integer userId;
-    private String userName;
-    private Integer parentId;
+import org.jspecify.annotations.Nullable;
 
-    public Integer getUserId() {
-        return userId;
-    }
-
-    public void setUserId(Integer userId) {
-        this.userId = userId;
-    }
-
-    public String getUserName() {
-        return userName;
-    }
-
-    public void setUserName(String userName) {
-        this.userName = userName;
-    }
-
-    public Integer getParentId() {
-        return parentId;
-    }
-
-    public void setParentId(Integer parentId) {
-        this.parentId = parentId;
-    }
-}
+public record User (Integer userId, String userName, @Nullable Integer parentId) {}
diff --git a/src/test/java/examples/joins/UserDynamicSQLSupport.java b/src/test/java/examples/joins/UserDynamicSQLSupport.java
index 45d511b3b..883cd81e9 100644
--- a/src/test/java/examples/joins/UserDynamicSQLSupport.java
+++ b/src/test/java/examples/joins/UserDynamicSQLSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/joins/package-info.java b/src/test/java/examples/joins/package-info.java
new file mode 100644
index 000000000..a994448fb
--- /dev/null
+++ b/src/test/java/examples/joins/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 examples.joins;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/src/test/java/examples/mariadb/ItemsDynamicSQLSupport.java b/src/test/java/examples/mariadb/ItemsDynamicSQLSupport.java
index 0ef660624..c47a1af3d 100644
--- a/src/test/java/examples/mariadb/ItemsDynamicSQLSupport.java
+++ b/src/test/java/examples/mariadb/ItemsDynamicSQLSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/mariadb/MariaDBTest.java b/src/test/java/examples/mariadb/MariaDBTest.java
index a7dca33e8..a98b2e326 100644
--- a/src/test/java/examples/mariadb/MariaDBTest.java
+++ b/src/test/java/examples/mariadb/MariaDBTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/schema_supplier/UserDynamicSqlSupport.java b/src/test/java/examples/mariadb/NumbersDynamicSQLSupport.java
similarity index 54%
rename from src/test/java/examples/schema_supplier/UserDynamicSqlSupport.java
rename to src/test/java/examples/mariadb/NumbersDynamicSQLSupport.java
index fdb38f0ea..b36b90ec6 100644
--- a/src/test/java/examples/schema_supplier/UserDynamicSqlSupport.java
+++ b/src/test/java/examples/mariadb/NumbersDynamicSQLSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -13,24 +13,24 @@
  *    See the License for the specific language governing permissions and
  *    limitations under the License.
  */
-package examples.schema_supplier;
-
-import java.sql.JDBCType;
+package examples.mariadb;
 
 import org.mybatis.dynamic.sql.SqlColumn;
 import org.mybatis.dynamic.sql.SqlTable;
 
-public class UserDynamicSqlSupport {
-    public static final User user = new User();
-    public static final SqlColumn<Integer> id = user.id;
-    public static final SqlColumn<String> name = user.name;
+import java.sql.JDBCType;
+
+public final class NumbersDynamicSQLSupport {
+    public static final Numbers numbers = new Numbers();
+    public static final SqlColumn<Integer> id = numbers.id;
+    public static final SqlColumn<String> description = numbers.description;
 
-    public static final class User extends SqlTable {
-        public final SqlColumn<Integer> id = column("user_id", JDBCType.INTEGER);
-        public final SqlColumn<String> name = column("user_name", JDBCType.VARCHAR);
+    public static final class Numbers extends SqlTable {
+        public final SqlColumn<Integer> id = column("id", JDBCType.INTEGER);
+        public final SqlColumn<String> description = column("description", JDBCType.VARCHAR);
 
-        public User() {
-            super(SchemaSupplier::schemaPropertyReader, "User");
+        public Numbers() {
+            super("numbers");
         }
     }
 }
diff --git a/src/test/java/examples/mariadb/OrderByCaseTest.java b/src/test/java/examples/mariadb/OrderByCaseTest.java
new file mode 100644
index 000000000..f842ecb1b
--- /dev/null
+++ b/src/test/java/examples/mariadb/OrderByCaseTest.java
@@ -0,0 +1,358 @@
+/*
+ *    Copyright 2016-2025 the original author or authors.
+ *
+ *    Licensed under the Apache License, Version 2.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 examples.mariadb;
+
+import static examples.mariadb.NumbersDynamicSQLSupport.description;
+import static examples.mariadb.NumbersDynamicSQLSupport.id;
+import static examples.mariadb.NumbersDynamicSQLSupport.numbers;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mybatis.dynamic.sql.SqlBuilder.case_;
+import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo;
+import static org.mybatis.dynamic.sql.SqlBuilder.select;
+
+import java.util.List;
+import java.util.Map;
+
+import config.TestContainersConfiguration;
+import org.apache.ibatis.datasource.unpooled.UnpooledDataSource;
+import org.apache.ibatis.mapping.Environment;
+import org.apache.ibatis.session.Configuration;
+import org.apache.ibatis.session.SqlSession;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.apache.ibatis.session.SqlSessionFactoryBuilder;
+import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mybatis.dynamic.sql.render.RenderingStrategies;
+import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
+import org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper;
+import org.testcontainers.containers.MariaDBContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+@Testcontainers
+class OrderByCaseTest {
+
+    @SuppressWarnings("resource")
+    @Container
+    private static final MariaDBContainer<?> mariadb =
+            new MariaDBContainer<>(TestContainersConfiguration.MARIADB_LATEST)
+                    .withInitScript("examples/mariadb/CreateDB.sql");
+
+    private static SqlSessionFactory sqlSessionFactory;
+
+    @BeforeAll
+    static void setup() {
+        UnpooledDataSource ds = new UnpooledDataSource(mariadb.getDriverClassName(), mariadb.getJdbcUrl(),
+                mariadb.getUsername(), mariadb.getPassword());
+        Environment environment = new Environment("test", new JdbcTransactionFactory(), ds);
+        Configuration config = new Configuration(environment);
+        config.addMapper(CommonSelectMapper.class);
+        sqlSessionFactory = new SqlSessionFactoryBuilder().build(config);
+    }
+
+    @Test
+    void testOrderBySimpleCase() {
+        try (SqlSession session = sqlSessionFactory.openSession()) {
+            CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class);
+
+            SelectStatementProvider selectStatement = select(id, description).from(numbers)
+                    .orderBy(case_(description)
+                            .when("One").then(3)
+                            .when("Two").then(5)
+                            .when("Three").then(4)
+                            .when("Four").then(2)
+                            .when("Five").then(1)
+                            .else_(99)
+                            .end())
+                    .build()
+                    .render(RenderingStrategies.MYBATIS3);
+
+            String expected = "select id, description from numbers order by case description "
+                    +  "when #{parameters.p1,jdbcType=VARCHAR} then 3 "
+                    +  "when #{parameters.p2,jdbcType=VARCHAR} then 5 "
+                    +  "when #{parameters.p3,jdbcType=VARCHAR} then 4 "
+                    +  "when #{parameters.p4,jdbcType=VARCHAR} then 2 "
+                    +  "when #{parameters.p5,jdbcType=VARCHAR} then 1 else 99 end";
+
+            assertThat(selectStatement.getSelectStatement()).isEqualTo( expected);
+
+            List<Map<String, Object>> rows = mapper.selectManyMappedRows(selectStatement);
+            assertThat(rows).hasSize(5);
+            assertThat(rows.get(0)).extracting("id", "description").containsExactly(5, "Five");
+            assertThat(rows.get(1)).extracting("id", "description").containsExactly(4, "Four");
+            assertThat(rows.get(2)).extracting("id", "description").containsExactly(1, "One");
+            assertThat(rows.get(3)).extracting("id", "description").containsExactly(3, "Three");
+            assertThat(rows.get(4)).extracting("id", "description").containsExactly(2, "Two");
+        }
+    }
+
+    @Test
+    void testOrderBySimpleCaseWithTableAlias() {
+        // ignore table aliases in order by phrases
+        try (SqlSession session = sqlSessionFactory.openSession()) {
+            CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class);
+
+            SelectStatementProvider selectStatement = select(id, description).from(numbers, "n")
+                    .orderBy(case_(description)
+                            .when("One").then(3)
+                            .when("Two").then(5)
+                            .when("Three").then(4)
+                            .when("Four").then(2)
+                            .when("Five").then(1)
+                            .else_(99)
+                            .end())
+                    .build()
+                    .render(RenderingStrategies.MYBATIS3);
+
+            String expected = "select n.id, n.description from numbers n order by case description "
+                    +  "when #{parameters.p1,jdbcType=VARCHAR} then 3 "
+                    +  "when #{parameters.p2,jdbcType=VARCHAR} then 5 "
+                    +  "when #{parameters.p3,jdbcType=VARCHAR} then 4 "
+                    +  "when #{parameters.p4,jdbcType=VARCHAR} then 2 "
+                    +  "when #{parameters.p5,jdbcType=VARCHAR} then 1 else 99 end";
+
+            assertThat(selectStatement.getSelectStatement()).isEqualTo( expected);
+
+            List<Map<String, Object>> rows = mapper.selectManyMappedRows(selectStatement);
+            assertThat(rows).hasSize(5);
+            assertThat(rows.get(0)).extracting("id", "description").containsExactly(5, "Five");
+            assertThat(rows.get(1)).extracting("id", "description").containsExactly(4, "Four");
+            assertThat(rows.get(2)).extracting("id", "description").containsExactly(1, "One");
+            assertThat(rows.get(3)).extracting("id", "description").containsExactly(3, "Three");
+            assertThat(rows.get(4)).extracting("id", "description").containsExactly(2, "Two");
+        }
+    }
+
+    @Test
+    void testOrderBySimpleCaseWithColumnAlias() {
+        // ignore table aliases in order by phrases
+        try (SqlSession session = sqlSessionFactory.openSession()) {
+            CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class);
+
+            SelectStatementProvider selectStatement = select(id, description.as("descr")).from(numbers)
+                    .orderBy(case_(description.as("descr"))
+                            .when("One").then(3)
+                            .when("Two").then(5)
+                            .when("Three").then(4)
+                            .when("Four").then(2)
+                            .when("Five").then(1)
+                            .else_(99)
+                            .end())
+                    .build()
+                    .render(RenderingStrategies.MYBATIS3);
+
+            String expected = "select id, description as descr from numbers order by case descr "
+                    +  "when #{parameters.p1,jdbcType=VARCHAR} then 3 "
+                    +  "when #{parameters.p2,jdbcType=VARCHAR} then 5 "
+                    +  "when #{parameters.p3,jdbcType=VARCHAR} then 4 "
+                    +  "when #{parameters.p4,jdbcType=VARCHAR} then 2 "
+                    +  "when #{parameters.p5,jdbcType=VARCHAR} then 1 else 99 end";
+
+            assertThat(selectStatement.getSelectStatement()).isEqualTo( expected);
+
+            List<Map<String, Object>> rows = mapper.selectManyMappedRows(selectStatement);
+            assertThat(rows).hasSize(5);
+            assertThat(rows.get(0)).extracting("id", "descr").containsExactly(5, "Five");
+            assertThat(rows.get(1)).extracting("id", "descr").containsExactly(4, "Four");
+            assertThat(rows.get(2)).extracting("id", "descr").containsExactly(1, "One");
+            assertThat(rows.get(3)).extracting("id", "descr").containsExactly(3, "Three");
+            assertThat(rows.get(4)).extracting("id", "descr").containsExactly(2, "Two");
+        }
+    }
+
+    @Test
+    void testOrderBySimpleCaseDescending() {
+        try (SqlSession session = sqlSessionFactory.openSession()) {
+            CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class);
+
+            SelectStatementProvider selectStatement = select(id, description).from(numbers)
+                    .orderBy(case_(description)
+                            .when("One").then(3)
+                            .when("Two").then(5)
+                            .when("Three").then(4)
+                            .when("Four").then(2)
+                            .when("Five").then(1)
+                            .else_(99)
+                            .end().descending())
+                    .build()
+                    .render(RenderingStrategies.MYBATIS3);
+
+            String expected = "select id, description from numbers order by case description "
+                    +  "when #{parameters.p1,jdbcType=VARCHAR} then 3 "
+                    +  "when #{parameters.p2,jdbcType=VARCHAR} then 5 "
+                    +  "when #{parameters.p3,jdbcType=VARCHAR} then 4 "
+                    +  "when #{parameters.p4,jdbcType=VARCHAR} then 2 "
+                    +  "when #{parameters.p5,jdbcType=VARCHAR} then 1 else 99 end DESC";
+
+            assertThat(selectStatement.getSelectStatement()).isEqualTo( expected);
+
+            List<Map<String, Object>> rows = mapper.selectManyMappedRows(selectStatement);
+            assertThat(rows).hasSize(5);
+            assertThat(rows.get(4)).extracting("id", "description").containsExactly(5, "Five");
+            assertThat(rows.get(3)).extracting("id", "description").containsExactly(4, "Four");
+            assertThat(rows.get(2)).extracting("id", "description").containsExactly(1, "One");
+            assertThat(rows.get(1)).extracting("id", "description").containsExactly(3, "Three");
+            assertThat(rows.get(0)).extracting("id", "description").containsExactly(2, "Two");
+        }
+    }
+
+    @Test
+    void testOrderBySearchedCase() {
+        try (SqlSession session = sqlSessionFactory.openSession()) {
+            CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class);
+
+            SelectStatementProvider selectStatement = select(id, description).from(numbers)
+                    .orderBy(case_()
+                            .when(description, isEqualTo("One")).then(3)
+                            .when(description, isEqualTo("Two")).then(5)
+                            .when(description, isEqualTo("Three")).then(4)
+                            .when(description, isEqualTo("Four")).then(2)
+                            .when(description, isEqualTo("Five")).then(1)
+                            .else_(99)
+                            .end())
+                    .build()
+                    .render(RenderingStrategies.MYBATIS3);
+
+            String expected = "select id, description from numbers order by case "
+                    +  "when description = #{parameters.p1,jdbcType=VARCHAR} then 3 "
+                    +  "when description = #{parameters.p2,jdbcType=VARCHAR} then 5 "
+                    +  "when description = #{parameters.p3,jdbcType=VARCHAR} then 4 "
+                    +  "when description = #{parameters.p4,jdbcType=VARCHAR} then 2 "
+                    +  "when description = #{parameters.p5,jdbcType=VARCHAR} then 1 else 99 end";
+
+            assertThat(selectStatement.getSelectStatement()).isEqualTo( expected);
+
+            List<Map<String, Object>> rows = mapper.selectManyMappedRows(selectStatement);
+            assertThat(rows).hasSize(5);
+            assertThat(rows.get(0)).extracting("id", "description").containsExactly(5, "Five");
+            assertThat(rows.get(1)).extracting("id", "description").containsExactly(4, "Four");
+            assertThat(rows.get(2)).extracting("id", "description").containsExactly(1, "One");
+            assertThat(rows.get(3)).extracting("id", "description").containsExactly(3, "Three");
+            assertThat(rows.get(4)).extracting("id", "description").containsExactly(2, "Two");
+        }
+    }
+
+    @Test
+    void testOrderBySearchedCaseWithTableAlias() {
+        // ignore table aliases in order by phrases
+        try (SqlSession session = sqlSessionFactory.openSession()) {
+            CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class);
+
+            SelectStatementProvider selectStatement = select(id, description).from(numbers, "n")
+                    .orderBy(case_()
+                            .when(description, isEqualTo("One")).then(3)
+                            .when(description, isEqualTo("Two")).then(5)
+                            .when(description, isEqualTo("Three")).then(4)
+                            .when(description, isEqualTo("Four")).then(2)
+                            .when(description, isEqualTo("Five")).then(1)
+                            .else_(99)
+                            .end())
+                    .build()
+                    .render(RenderingStrategies.MYBATIS3);
+
+            String expected = "select n.id, n.description from numbers n order by case "
+                    +  "when description = #{parameters.p1,jdbcType=VARCHAR} then 3 "
+                    +  "when description = #{parameters.p2,jdbcType=VARCHAR} then 5 "
+                    +  "when description = #{parameters.p3,jdbcType=VARCHAR} then 4 "
+                    +  "when description = #{parameters.p4,jdbcType=VARCHAR} then 2 "
+                    +  "when description = #{parameters.p5,jdbcType=VARCHAR} then 1 else 99 end";
+
+            assertThat(selectStatement.getSelectStatement()).isEqualTo( expected);
+
+            List<Map<String, Object>> rows = mapper.selectManyMappedRows(selectStatement);
+            assertThat(rows).hasSize(5);
+            assertThat(rows.get(0)).extracting("id", "description").containsExactly(5, "Five");
+            assertThat(rows.get(1)).extracting("id", "description").containsExactly(4, "Four");
+            assertThat(rows.get(2)).extracting("id", "description").containsExactly(1, "One");
+            assertThat(rows.get(3)).extracting("id", "description").containsExactly(3, "Three");
+            assertThat(rows.get(4)).extracting("id", "description").containsExactly(2, "Two");
+        }
+    }
+
+    @Test
+    void testOrderBySearchedCaseWithColumnAlias() {
+        // ignore table aliases in order by phrases
+        try (SqlSession session = sqlSessionFactory.openSession()) {
+            CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class);
+
+            SelectStatementProvider selectStatement = select(id, description.as("descr")).from(numbers)
+                    .orderBy(case_()
+                            .when(description.as("descr"), isEqualTo("One")).then(3)
+                            .when(description.as("descr"), isEqualTo("Two")).then(5)
+                            .when(description.as("descr"), isEqualTo("Three")).then(4)
+                            .when(description.as("descr"), isEqualTo("Four")).then(2)
+                            .when(description.as("descr"), isEqualTo("Five")).then(1)
+                            .else_(99)
+                            .end())
+                    .build()
+                    .render(RenderingStrategies.MYBATIS3);
+
+            String expected = "select id, description as descr from numbers order by case "
+                    +  "when descr = #{parameters.p1,jdbcType=VARCHAR} then 3 "
+                    +  "when descr = #{parameters.p2,jdbcType=VARCHAR} then 5 "
+                    +  "when descr = #{parameters.p3,jdbcType=VARCHAR} then 4 "
+                    +  "when descr = #{parameters.p4,jdbcType=VARCHAR} then 2 "
+                    +  "when descr = #{parameters.p5,jdbcType=VARCHAR} then 1 else 99 end";
+
+            assertThat(selectStatement.getSelectStatement()).isEqualTo( expected);
+
+            List<Map<String, Object>> rows = mapper.selectManyMappedRows(selectStatement);
+            assertThat(rows).hasSize(5);
+            assertThat(rows.get(0)).extracting("id", "descr").containsExactly(5, "Five");
+            assertThat(rows.get(1)).extracting("id", "descr").containsExactly(4, "Four");
+            assertThat(rows.get(2)).extracting("id", "descr").containsExactly(1, "One");
+            assertThat(rows.get(3)).extracting("id", "descr").containsExactly(3, "Three");
+            assertThat(rows.get(4)).extracting("id", "descr").containsExactly(2, "Two");
+        }
+    }
+
+    @Test
+    void testOrderBySearchedCaseDescending() {
+        try (SqlSession session = sqlSessionFactory.openSession()) {
+            CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class);
+
+            SelectStatementProvider selectStatement = select(id, description).from(numbers)
+                    .orderBy(case_()
+                            .when(description, isEqualTo("One")).then(3)
+                            .when(description, isEqualTo("Two")).then(5)
+                            .when(description, isEqualTo("Three")).then(4)
+                            .when(description, isEqualTo("Four")).then(2)
+                            .when(description, isEqualTo("Five")).then(1)
+                            .else_(99)
+                            .end().descending())
+                    .build()
+                    .render(RenderingStrategies.MYBATIS3);
+
+            String expected = "select id, description from numbers order by case "
+                    +  "when description = #{parameters.p1,jdbcType=VARCHAR} then 3 "
+                    +  "when description = #{parameters.p2,jdbcType=VARCHAR} then 5 "
+                    +  "when description = #{parameters.p3,jdbcType=VARCHAR} then 4 "
+                    +  "when description = #{parameters.p4,jdbcType=VARCHAR} then 2 "
+                    +  "when description = #{parameters.p5,jdbcType=VARCHAR} then 1 else 99 end DESC";
+
+            assertThat(selectStatement.getSelectStatement()).isEqualTo( expected);
+
+            List<Map<String, Object>> rows = mapper.selectManyMappedRows(selectStatement);
+            assertThat(rows).hasSize(5);
+            assertThat(rows.get(4)).extracting("id", "description").containsExactly(5, "Five");
+            assertThat(rows.get(3)).extracting("id", "description").containsExactly(4, "Four");
+            assertThat(rows.get(2)).extracting("id", "description").containsExactly(1, "One");
+            assertThat(rows.get(1)).extracting("id", "description").containsExactly(3, "Three");
+            assertThat(rows.get(0)).extracting("id", "description").containsExactly(2, "Two");
+        }
+    }
+}
diff --git a/src/test/java/examples/mysql/IsLikeEscape.java b/src/test/java/examples/mysql/IsLikeEscape.java
new file mode 100644
index 000000000..418ff7141
--- /dev/null
+++ b/src/test/java/examples/mysql/IsLikeEscape.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 examples.mysql;
+
+import java.util.NoSuchElementException;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import org.jspecify.annotations.Nullable;
+import org.mybatis.dynamic.sql.AbstractSingleValueCondition;
+import org.mybatis.dynamic.sql.BindableColumn;
+import org.mybatis.dynamic.sql.render.RenderingContext;
+import org.mybatis.dynamic.sql.util.FragmentAndParameters;
+
+public class IsLikeEscape<T> extends AbstractSingleValueCondition<T>
+        implements AbstractSingleValueCondition.Filterable<T>, AbstractSingleValueCondition.Mappable<T> {
+    private static final IsLikeEscape<?> EMPTY = new IsLikeEscape<Object>(-1, null) {
+        @Override
+        public Object value() {
+            throw new NoSuchElementException("No value present"); //$NON-NLS-1$
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return true;
+        }
+    };
+
+    public static <T> IsLikeEscape<T> empty() {
+        @SuppressWarnings("unchecked")
+        IsLikeEscape<T> t = (IsLikeEscape<T>) 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<T> 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<T> filter(Predicate<? super T> predicate) {
+        return filterSupport(predicate, IsLikeEscape::empty, this);
+    }
+
+    @Override
+    public <R> IsLikeEscape<R> map(Function<? super T, ? extends R> mapper) {
+        return mapSupport(mapper, v -> new IsLikeEscape<>(v, escapeCharacter), IsLikeEscape::empty);
+    }
+
+    public static <T> IsLikeEscape<T> isLike(T value) {
+        return new IsLikeEscape<>(value, null);
+    }
+
+    public static <T> IsLikeEscape<T> isLike(T value, Character escapeCharacter) {
+        return new IsLikeEscape<>(value, escapeCharacter);
+    }
+}
diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/TypedJoinCondition.java b/src/test/java/examples/mysql/MemberOfCondition.java
similarity index 53%
rename from src/main/java/org/mybatis/dynamic/sql/select/join/TypedJoinCondition.java
rename to src/test/java/examples/mysql/MemberOfCondition.java
index 12b310d1f..962ad5c38 100644
--- a/src/main/java/org/mybatis/dynamic/sql/select/join/TypedJoinCondition.java
+++ b/src/test/java/examples/mysql/MemberOfCondition.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -13,23 +13,25 @@
  *    See the License for the specific language governing permissions and
  *    limitations under the License.
  */
-package org.mybatis.dynamic.sql.select.join;
+package examples.mysql;
 
 import java.util.Objects;
 
-public abstract class TypedJoinCondition<T> implements JoinCondition<T> {
-    private final T value;
+import org.mybatis.dynamic.sql.AbstractNoValueCondition;
 
-    protected TypedJoinCondition(T value) {
-        this.value = Objects.requireNonNull(value);
-    }
+public class MemberOfCondition<T> extends AbstractNoValueCondition<T> {
+    private final String jsonArray;
 
-    public T value() {
-        return value;
+    protected MemberOfCondition(String jsonArray) {
+        this.jsonArray = Objects.requireNonNull(jsonArray);
     }
 
     @Override
-    public <R> R accept(JoinConditionVisitor<T, R> visitor) {
-        return visitor.visit(this);
+    public String operator() {
+        return "member of(" + jsonArray + ")";
+    }
+
+    public static <T> MemberOfCondition<T> memberOf(String jsonArray) {
+        return new MemberOfCondition<>(jsonArray);
     }
 }
diff --git a/src/test/java/examples/mysql/MemberOfFunction.java b/src/test/java/examples/mysql/MemberOfFunction.java
new file mode 100644
index 000000000..d41f95f8c
--- /dev/null
+++ b/src/test/java/examples/mysql/MemberOfFunction.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 examples.mysql;
+
+import java.util.Objects;
+
+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 MemberOfFunction<T> extends AbstractTypeConvertingFunction<T, Long, MemberOfFunction<T>> {
+
+    private final String jsonArray;
+
+    protected MemberOfFunction(BasicColumn column, String jsonArray) {
+        super(column);
+        this.jsonArray = Objects.requireNonNull(jsonArray);
+    }
+
+    @Override
+    protected MemberOfFunction<T> copy() {
+        return new MemberOfFunction<>(column, jsonArray);
+    }
+
+    @Override
+    public FragmentAndParameters render(RenderingContext renderingContext) {
+        return column.render(renderingContext)
+                .mapFragment(f -> f + " member of(" + jsonArray + ")");
+    }
+
+    public static <T> MemberOfFunction<T> memberOf(BindableColumn<T> column, String jsonArray) {
+        return new MemberOfFunction<>(column, jsonArray);
+    }
+}
diff --git a/src/test/java/examples/mysql/MySQLTest.java b/src/test/java/examples/mysql/MySQLTest.java
new file mode 100644
index 000000000..f9c047f77
--- /dev/null
+++ b/src/test/java/examples/mysql/MySQLTest.java
@@ -0,0 +1,143 @@
+/*
+ *    Copyright 2016-2025 the original author or authors.
+ *
+ *    Licensed under the Apache License, Version 2.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 examples.mysql;
+
+import static examples.mysql.MemberOfCondition.memberOf;
+import static examples.mysql.MemberOfFunction.memberOf;
+import static examples.mariadb.ItemsDynamicSQLSupport.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.entry;
+import static org.mybatis.dynamic.sql.SqlBuilder.*;
+
+import java.util.List;
+import java.util.Map;
+
+import config.TestContainersConfiguration;
+import org.apache.ibatis.datasource.unpooled.UnpooledDataSource;
+import org.apache.ibatis.mapping.Environment;
+import org.apache.ibatis.session.Configuration;
+import org.apache.ibatis.session.SqlSession;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.apache.ibatis.session.SqlSessionFactoryBuilder;
+import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mybatis.dynamic.sql.render.RenderingStrategies;
+import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
+import org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper;
+import org.testcontainers.containers.MySQLContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+@Testcontainers
+class MySQLTest {
+
+    @SuppressWarnings("resource")
+    @Container
+    private static final MySQLContainer<?> mysql =
+            new MySQLContainer<>(TestContainersConfiguration.MYSQL_LATEST)
+                    .withInitScript("examples/mariadb/CreateDB.sql");
+
+    private SqlSessionFactory sqlSessionFactory;
+
+    @BeforeEach
+    void setup() {
+        UnpooledDataSource ds = new UnpooledDataSource(mysql.getDriverClassName(), mysql.getJdbcUrl(),
+                mysql.getUsername(), mysql.getPassword());
+        Environment environment = new Environment("test", new JdbcTransactionFactory(), ds);
+        Configuration config = new Configuration(environment);
+        config.addMapper(CommonSelectMapper.class);
+        sqlSessionFactory = new SqlSessionFactoryBuilder().build(config);
+    }
+
+    @Test
+    void smokeTest() {
+        try (SqlSession session = sqlSessionFactory.openSession()) {
+            CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class);
+
+            SelectStatementProvider selectStatement = select(id, description)
+                    .from(items)
+                    .orderBy(id)
+                    .build()
+                    .render(RenderingStrategies.MYBATIS3);
+             List<Map<String, Object>> rows = mapper.selectManyMappedRows(selectStatement);
+             assertThat(rows).hasSize(20);
+        }
+    }
+
+    @Test
+    void testMemberOfAsCondition() {
+        try (SqlSession session = sqlSessionFactory.openSession()) {
+            CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class);
+
+            SelectStatementProvider selectStatement = select(id, memberOf(id, "'[1, 2, 3]'").as("inList"))
+                    .from(items)
+                    .where(id, memberOf("'[1, 2, 3]'"))
+                    .orderBy(id)
+                    .build()
+                    .render(RenderingStrategies.MYBATIS3);
+
+            assertThat(selectStatement.getSelectStatement())
+                    .isEqualTo("select id, id member of('[1, 2, 3]') as inList from items where id member of('[1, 2, 3]') order by id");
+
+            List<Map<String, Object>> rows = mapper.selectManyMappedRows(selectStatement);
+            assertThat(rows).hasSize(3);
+            assertThat(rows.get(2)).containsOnly(entry("id", 3), entry("inList", 1L));
+        }
+    }
+
+    @Test
+    void testMemberOfAsFunction() {
+        try (SqlSession session = sqlSessionFactory.openSession()) {
+            CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class);
+
+            SelectStatementProvider selectStatement = select(id, memberOf(id, "'[1, 2, 3]'").as("inList"))
+                    .from(items)
+                    .where(memberOf(id,"'[1, 2, 3]'"), isEqualTo(1L))
+                    .orderBy(id)
+                    .build()
+                    .render(RenderingStrategies.MYBATIS3);
+
+            assertThat(selectStatement.getSelectStatement())
+                    .isEqualTo("select id, id member of('[1, 2, 3]') as inList from items where id member of('[1, 2, 3]') = #{parameters.p1} order by id");
+
+            List<Map<String, Object>> rows = mapper.selectManyMappedRows(selectStatement);
+            assertThat(rows).hasSize(3);
+            assertThat(rows.get(2)).containsOnly(entry("id", 3), entry("inList", 1L));
+        }
+    }
+
+    @Test
+    void testIsLikeEscape() {
+        try (SqlSession session = sqlSessionFactory.openSession()) {
+            CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class);
+
+            SelectStatementProvider selectStatement = select(id, description)
+                    .from(items)
+                    .where(description, IsLikeEscape.isLike("Item 1%", '#').map(s -> s))
+                    .orderBy(id)
+                    .build()
+                    .render(RenderingStrategies.MYBATIS3);
+
+            assertThat(selectStatement.getSelectStatement())
+                    .isEqualTo("select id, description from items where description like #{parameters.p1,jdbcType=VARCHAR} ESCAPE '#' order by id");
+
+            List<Map<String, Object>> rows = mapper.selectManyMappedRows(selectStatement);
+            assertThat(rows).hasSize(11);
+            assertThat(rows.get(2)).containsOnly(entry("id", 11), entry("description", "Item 11"));
+        }
+    }
+}
diff --git a/src/test/java/examples/mysql/package-info.java b/src/test/java/examples/mysql/package-info.java
new file mode 100644
index 000000000..70357075d
--- /dev/null
+++ b/src/test/java/examples/mysql/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 examples.mysql;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/src/test/java/examples/paging/LimitAndOffsetAdapter.java b/src/test/java/examples/paging/LimitAndOffsetAdapter.java
index 02a7cf7d8..de4dafab7 100644
--- a/src/test/java/examples/paging/LimitAndOffsetAdapter.java
+++ b/src/test/java/examples/paging/LimitAndOffsetAdapter.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/paging/LimitAndOffsetMapper.java b/src/test/java/examples/paging/LimitAndOffsetMapper.java
index ef900e827..14bd2d8b0 100644
--- a/src/test/java/examples/paging/LimitAndOffsetMapper.java
+++ b/src/test/java/examples/paging/LimitAndOffsetMapper.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,8 +19,7 @@
 
 import java.util.List;
 
-import org.apache.ibatis.annotations.Result;
-import org.apache.ibatis.annotations.Results;
+import org.apache.ibatis.annotations.Arg;
 import org.apache.ibatis.annotations.SelectProvider;
 import org.mybatis.dynamic.sql.select.QueryExpressionDSL;
 import org.mybatis.dynamic.sql.select.SelectDSL;
@@ -32,12 +31,10 @@
 public interface LimitAndOffsetMapper {
 
     @SelectProvider(type=SqlProviderAdapter.class, method="select")
-    @Results(id="AnimalDataResult", value={
-        @Result(column="id", property="id", id=true),
-        @Result(column="animal_name", property="animalName"),
-        @Result(column="brain_weight", property="brainWeight"),
-        @Result(column="body_weight", property="bodyWeight")
-    })
+    @Arg(column = "id", javaType = int.class, id = true)
+    @Arg(column = "animal_name", javaType = String.class)
+    @Arg(column = "brain_weight", javaType = double.class)
+    @Arg(column = "body_weight", javaType = double.class)
     List<AnimalData> selectMany(SelectStatementProvider selectStatement);
 
     default QueryExpressionDSL<LimitAndOffsetAdapter<List<AnimalData>>> selectWithLimitAndOffset(int limit, int offset) {
diff --git a/src/test/java/examples/paging/LimitAndOffsetTest.java b/src/test/java/examples/paging/LimitAndOffsetTest.java
index f5a3ab96a..6e8d68ae1 100644
--- a/src/test/java/examples/paging/LimitAndOffsetTest.java
+++ b/src/test/java/examples/paging/LimitAndOffsetTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -73,7 +73,7 @@ void testLimitAndOffset() {
                     .execute();
 
             assertThat(rows).hasSize(5);
-            assertThat(rows.get(0).getId()).isEqualTo(4);
+            assertThat(rows.get(0).id()).isEqualTo(4);
         }
     }
 }
diff --git a/src/test/java/examples/paging/package-info.java b/src/test/java/examples/paging/package-info.java
new file mode 100644
index 000000000..c8f8f4390
--- /dev/null
+++ b/src/test/java/examples/paging/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 examples.paging;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/src/test/java/examples/postgres/PostgresTest.java b/src/test/java/examples/postgres/PostgresTest.java
new file mode 100644
index 000000000..f8da9c328
--- /dev/null
+++ b/src/test/java/examples/postgres/PostgresTest.java
@@ -0,0 +1,298 @@
+/*
+ *    Copyright 2016-2025 the original author or authors.
+ *
+ *    Licensed under the Apache License, Version 2.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 examples.postgres;
+
+import static examples.postgres.TableCodeDynamicSqlSupport.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mybatis.dynamic.sql.SqlBuilder.*;
+
+import java.util.List;
+import java.util.Map;
+
+import config.TestContainersConfiguration;
+import org.apache.ibatis.datasource.unpooled.UnpooledDataSource;
+import org.apache.ibatis.mapping.Environment;
+import org.apache.ibatis.session.Configuration;
+import org.apache.ibatis.session.SqlSession;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.apache.ibatis.session.SqlSessionFactoryBuilder;
+import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mybatis.dynamic.sql.render.RenderingStrategies;
+import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
+import org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper;
+import org.testcontainers.containers.PostgreSQLContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+@Testcontainers
+class PostgresTest {
+
+    @SuppressWarnings("resource")
+    @Container
+    private static final PostgreSQLContainer<?> postgres =
+            new PostgreSQLContainer<>(TestContainersConfiguration.POSTGRES_LATEST)
+                    .withInitScript("examples/postgres/dbInit.sql");
+
+    private static SqlSessionFactory sqlSessionFactory;
+
+    @BeforeAll
+    static void setUp() {
+        UnpooledDataSource ds = new UnpooledDataSource(postgres.getDriverClassName(), postgres.getJdbcUrl(),
+                postgres.getUsername(), postgres.getPassword());
+        Environment environment = new Environment("test", new JdbcTransactionFactory(), ds);
+        Configuration configuration = new Configuration(environment);
+        configuration.addMapper(CommonSelectMapper.class);
+        sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
+    }
+
+    @Test
+    void testSelectForUpdate() {
+        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
+            CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class);
+
+            SelectStatementProvider selectStatement = select(id, description)
+                    .from(tableCode)
+                    .orderBy(id)
+                    .forUpdate()
+                    .build()
+                    .render(RenderingStrategies.MYBATIS3);
+
+            assertThat(selectStatement.getSelectStatement())
+                    .isEqualTo("select id, description from TableCode order by id for update");
+            List<Map<String, Object>> records = mapper.selectManyMappedRows(selectStatement);
+            assertThat(records).hasSize(4);
+        }
+    }
+
+    @Test
+    void testSelectForUpdateNoWait() {
+        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
+            CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class);
+
+            SelectStatementProvider selectStatement = select(id, description)
+                    .from(tableCode)
+                    .orderBy(id)
+                    .forUpdate()
+                    .nowait()
+                    .build()
+                    .render(RenderingStrategies.MYBATIS3);
+
+            assertThat(selectStatement.getSelectStatement())
+                    .isEqualTo("select id, description from TableCode order by id for update nowait");
+            List<Map<String, Object>> records = mapper.selectManyMappedRows(selectStatement);
+            assertThat(records).hasSize(4);
+        }
+    }
+
+    @Test
+    void testSelectForUpdateSkipLocked() {
+        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
+            CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class);
+
+            SelectStatementProvider selectStatement = select(id, description)
+                    .from(tableCode)
+                    .orderBy(id)
+                    .forUpdate()
+                    .skipLocked()
+                    .build()
+                    .render(RenderingStrategies.MYBATIS3);
+
+            assertThat(selectStatement.getSelectStatement())
+                    .isEqualTo("select id, description from TableCode order by id for update skip locked");
+            List<Map<String, Object>> records = mapper.selectManyMappedRows(selectStatement);
+            assertThat(records).hasSize(4);
+        }
+    }
+
+    @Test
+    void testSelectForNoKeyUpdate() {
+        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
+            CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class);
+
+            SelectStatementProvider selectStatement = select(id, description)
+                    .from(tableCode)
+                    .orderBy(id)
+                    .forNoKeyUpdate()
+                    .build()
+                    .render(RenderingStrategies.MYBATIS3);
+
+            assertThat(selectStatement.getSelectStatement())
+                    .isEqualTo("select id, description from TableCode order by id for no key update");
+            List<Map<String, Object>> records = mapper.selectManyMappedRows(selectStatement);
+            assertThat(records).hasSize(4);
+        }
+    }
+
+    @Test
+    void testSelectForNoKeyUpdateNoWait() {
+        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
+            CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class);
+
+            SelectStatementProvider selectStatement = select(id, description)
+                    .from(tableCode)
+                    .orderBy(id)
+                    .forNoKeyUpdate()
+                    .nowait()
+                    .build()
+                    .render(RenderingStrategies.MYBATIS3);
+
+            assertThat(selectStatement.getSelectStatement())
+                    .isEqualTo("select id, description from TableCode order by id for no key update nowait");
+            List<Map<String, Object>> records = mapper.selectManyMappedRows(selectStatement);
+            assertThat(records).hasSize(4);
+        }
+    }
+
+    @Test
+    void testSelectForNoKeyUpdateSkipLocked() {
+        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
+            CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class);
+
+            SelectStatementProvider selectStatement = select(id, description)
+                    .from(tableCode)
+                    .orderBy(id)
+                    .forNoKeyUpdate()
+                    .skipLocked()
+                    .build()
+                    .render(RenderingStrategies.MYBATIS3);
+
+            assertThat(selectStatement.getSelectStatement())
+                    .isEqualTo("select id, description from TableCode order by id for no key update skip locked");
+            List<Map<String, Object>> records = mapper.selectManyMappedRows(selectStatement);
+            assertThat(records).hasSize(4);
+        }
+    }
+
+    @Test
+    void testSelectForShare() {
+        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
+            CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class);
+
+            SelectStatementProvider selectStatement = select(id, description)
+                    .from(tableCode)
+                    .orderBy(id)
+                    .forShare()
+                    .build()
+                    .render(RenderingStrategies.MYBATIS3);
+
+            assertThat(selectStatement.getSelectStatement())
+                    .isEqualTo("select id, description from TableCode order by id for share");
+            List<Map<String, Object>> records = mapper.selectManyMappedRows(selectStatement);
+            assertThat(records).hasSize(4);
+        }
+    }
+
+    @Test
+    void testSelectForShareNoWait() {
+        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
+            CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class);
+
+            SelectStatementProvider selectStatement = select(id, description)
+                    .from(tableCode)
+                    .orderBy(id)
+                    .forShare()
+                    .nowait()
+                    .build()
+                    .render(RenderingStrategies.MYBATIS3);
+
+            assertThat(selectStatement.getSelectStatement())
+                    .isEqualTo("select id, description from TableCode order by id for share nowait");
+            List<Map<String, Object>> records = mapper.selectManyMappedRows(selectStatement);
+            assertThat(records).hasSize(4);
+        }
+    }
+
+    @Test
+    void testSelectForShareSkipLocked() {
+        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
+            CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class);
+
+            SelectStatementProvider selectStatement = select(id, description)
+                    .from(tableCode)
+                    .orderBy(id)
+                    .forShare()
+                    .skipLocked()
+                    .build()
+                    .render(RenderingStrategies.MYBATIS3);
+
+            assertThat(selectStatement.getSelectStatement())
+                    .isEqualTo("select id, description from TableCode order by id for share skip locked");
+            List<Map<String, Object>> records = mapper.selectManyMappedRows(selectStatement);
+            assertThat(records).hasSize(4);
+        }
+    }
+
+    @Test
+    void testSelectForKeyShare() {
+        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
+            CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class);
+
+            SelectStatementProvider selectStatement = select(id, description)
+                    .from(tableCode)
+                    .orderBy(id)
+                    .forKeyShare()
+                    .build()
+                    .render(RenderingStrategies.MYBATIS3);
+
+            assertThat(selectStatement.getSelectStatement())
+                    .isEqualTo("select id, description from TableCode order by id for key share");
+            List<Map<String, Object>> records = mapper.selectManyMappedRows(selectStatement);
+            assertThat(records).hasSize(4);
+        }
+    }
+
+    @Test
+    void testSelectForKeyShareNoWait() {
+        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
+            CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class);
+
+            SelectStatementProvider selectStatement = select(id, description)
+                    .from(tableCode)
+                    .orderBy(id)
+                    .forKeyShare()
+                    .nowait()
+                    .build()
+                    .render(RenderingStrategies.MYBATIS3);
+
+            assertThat(selectStatement.getSelectStatement())
+                    .isEqualTo("select id, description from TableCode order by id for key share nowait");
+            List<Map<String, Object>> records = mapper.selectManyMappedRows(selectStatement);
+            assertThat(records).hasSize(4);
+        }
+    }
+
+    @Test
+    void testSelectForKeyShareSkipLocked() {
+        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
+            CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class);
+
+            SelectStatementProvider selectStatement = select(id, description)
+                    .from(tableCode)
+                    .orderBy(id)
+                    .forKeyShare()
+                    .skipLocked()
+                    .build()
+                    .render(RenderingStrategies.MYBATIS3);
+
+            assertThat(selectStatement.getSelectStatement())
+                    .isEqualTo("select id, description from TableCode order by id for key share skip locked");
+            List<Map<String, Object>> records = mapper.selectManyMappedRows(selectStatement);
+            assertThat(records).hasSize(4);
+        }
+    }
+}
diff --git a/src/test/java/examples/postgres/TableCodeDynamicSqlSupport.java b/src/test/java/examples/postgres/TableCodeDynamicSqlSupport.java
new file mode 100644
index 000000000..c2fd0deef
--- /dev/null
+++ b/src/test/java/examples/postgres/TableCodeDynamicSqlSupport.java
@@ -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 examples.postgres;
+
+import java.sql.JDBCType;
+
+import org.mybatis.dynamic.sql.SqlColumn;
+import org.mybatis.dynamic.sql.SqlTable;
+
+public final class TableCodeDynamicSqlSupport {
+    public static final TableCode tableCode = new TableCode();
+    public static final SqlColumn<Integer> id = tableCode.id;
+    public static final SqlColumn<String> description = tableCode.description;
+
+    public static class TableCode extends SqlTable {
+        public final SqlColumn<Integer> id = column("id", JDBCType.INTEGER);
+        public final SqlColumn<String> description = column("description", JDBCType.VARCHAR);
+
+        public TableCode() {
+            super("TableCode");
+        }
+    }
+}
diff --git a/src/test/java/examples/schema_supplier/SchemaSupplierTest.java b/src/test/java/examples/schema_supplier/SchemaSupplierTest.java
deleted file mode 100644
index 7cc70fc7c..000000000
--- a/src/test/java/examples/schema_supplier/SchemaSupplierTest.java
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- *    Copyright 2016-2024 the original author or authors.
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *       https://www.apache.org/licenses/LICENSE-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 examples.schema_supplier;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.sql.Connection;
-import java.sql.DriverManager;
-import java.util.List;
-
-import org.apache.ibatis.datasource.unpooled.UnpooledDataSource;
-import org.apache.ibatis.exceptions.PersistenceException;
-import org.apache.ibatis.jdbc.ScriptRunner;
-import org.apache.ibatis.mapping.Environment;
-import org.apache.ibatis.session.Configuration;
-import org.apache.ibatis.session.SqlSession;
-import org.apache.ibatis.session.SqlSessionFactory;
-import org.apache.ibatis.session.SqlSessionFactoryBuilder;
-import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.mybatis.dynamic.sql.select.SelectDSLCompleter;
-
-class SchemaSupplierTest {
-
-    private static final String JDBC_URL = "jdbc:hsqldb:mem:aname";
-    private static final String JDBC_DRIVER = "org.hsqldb.jdbcDriver";
-
-    private SqlSessionFactory sqlSessionFactory;
-
-    @BeforeEach
-    void setup() throws Exception {
-        Class.forName(JDBC_DRIVER);
-        InputStream is = getClass().getResourceAsStream("/examples/schema_supplier/CreateDB.sql");
-        assert is != null;
-        try (Connection connection = DriverManager.getConnection(JDBC_URL, "sa", "")) {
-            ScriptRunner sr = new ScriptRunner(connection);
-            sr.setLogWriter(null);
-            sr.runScript(new InputStreamReader(is));
-        }
-
-        UnpooledDataSource ds = new UnpooledDataSource(JDBC_DRIVER, JDBC_URL, "sa", "");
-        Environment environment = new Environment("test", new JdbcTransactionFactory(), ds);
-        Configuration config = new Configuration(environment);
-        config.addMapper(UserMapper.class);
-        sqlSessionFactory = new SqlSessionFactoryBuilder().build(config);
-    }
-
-    @Test
-    void testUnsetSchemaProperty() {
-        System.clearProperty(SchemaSupplier.schema_property);
-
-        try (SqlSession session = sqlSessionFactory.openSession()) {
-            UserMapper mapper = session.getMapper(UserMapper.class);
-
-            User user = new User();
-            user.setId(1);
-            user.setName("Fred");
-
-            assertThrows(PersistenceException.class, () -> mapper.insert(user));
-        }
-    }
-
-    @Test
-    void testSchemaProperty() {
-        System.setProperty(SchemaSupplier.schema_property, "schema1");
-
-        try (SqlSession session = sqlSessionFactory.openSession()) {
-            UserMapper mapper = session.getMapper(UserMapper.class);
-
-            insertFlintstones(mapper);
-
-            List<User> records = mapper.select(SelectDSLCompleter.allRows());
-            assertThat(records).hasSize(2);
-        }
-    }
-
-    @Test
-    void testSchemaSwitchingProperty() {
-        try (SqlSession session = sqlSessionFactory.openSession()) {
-            UserMapper mapper = session.getMapper(UserMapper.class);
-
-            System.setProperty(SchemaSupplier.schema_property, "schema1");
-            insertFlintstones(mapper);
-
-            List<User> records = mapper.select(SelectDSLCompleter.allRows());
-            assertThat(records).hasSize(2);
-
-            System.setProperty(SchemaSupplier.schema_property, "schema2");
-            insertRubbles(mapper);
-
-            records = mapper.select(SelectDSLCompleter.allRows());
-            assertThat(records).hasSize(3);
-        }
-    }
-
-    private void insertFlintstones(UserMapper mapper) {
-        User user = new User();
-        user.setId(1);
-        user.setName("Fred");
-        int rows = mapper.insert(user);
-        assertThat(rows).isEqualTo(1);
-
-        user = new User();
-        user.setId(2);
-        user.setName("Wilma");
-        rows = mapper.insert(user);
-        assertThat(rows).isEqualTo(1);
-    }
-
-    private void insertRubbles(UserMapper mapper) {
-        User user = new User();
-        user.setId(1);
-        user.setName("Barney");
-        int rows = mapper.insert(user);
-        assertThat(rows).isEqualTo(1);
-
-        user = new User();
-        user.setId(2);
-        user.setName("Betty");
-        rows = mapper.insert(user);
-        assertThat(rows).isEqualTo(1);
-
-        user = new User();
-        user.setId(3);
-        user.setName("Bamm Bamm");
-        rows = mapper.insert(user);
-        assertThat(rows).isEqualTo(1);
-    }
-}
diff --git a/src/test/java/examples/schema_supplier/UserMapper.java b/src/test/java/examples/schema_supplier/UserMapper.java
deleted file mode 100644
index 602ed49a3..000000000
--- a/src/test/java/examples/schema_supplier/UserMapper.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- *    Copyright 2016-2024 the original author or authors.
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *       https://www.apache.org/licenses/LICENSE-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 examples.schema_supplier;
-
-import static examples.schema_supplier.UserDynamicSqlSupport.*;
-
-import java.util.List;
-
-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.BasicColumn;
-import org.mybatis.dynamic.sql.SqlBuilder;
-import org.mybatis.dynamic.sql.render.RenderingStrategies;
-import org.mybatis.dynamic.sql.select.SelectDSLCompleter;
-import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
-import org.mybatis.dynamic.sql.util.SqlProviderAdapter;
-import org.mybatis.dynamic.sql.util.mybatis3.CommonInsertMapper;
-import org.mybatis.dynamic.sql.util.mybatis3.MyBatis3Utils;
-
-public interface UserMapper extends CommonInsertMapper<User> {
-
-    @SelectProvider(type=SqlProviderAdapter.class, method="select")
-    @Results(id="UserResult", value= {
-            @Result(column="USER_ID", property="id", jdbcType=JdbcType.INTEGER, id=true),
-            @Result(column="USER_NAME", property="name", jdbcType=JdbcType.VARCHAR)
-    })
-    List<User> selectMany(SelectStatementProvider selectStatement);
-
-    default List<User> select(SelectDSLCompleter completer) {
-        return MyBatis3Utils.selectList(this::selectMany, BasicColumn.columnList(id, name), user, completer);
-    }
-
-    default int insert(User row) {
-        return insert(SqlBuilder.insert(row)
-                .into(user)
-                .map(id).toProperty("id")
-                .map(name).toProperty("name")
-                .build()
-                .render(RenderingStrategies.MYBATIS3));
-    }
-}
diff --git a/src/test/java/examples/sharding/ShardedMapper.java b/src/test/java/examples/sharding/ShardedMapper.java
index b8bfe1e4f..0f271417c 100644
--- a/src/test/java/examples/sharding/ShardedMapper.java
+++ b/src/test/java/examples/sharding/ShardedMapper.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/sharding/ShardingTest.java b/src/test/java/examples/sharding/ShardingTest.java
index 86afdcf66..e3db9106a 100644
--- a/src/test/java/examples/sharding/ShardingTest.java
+++ b/src/test/java/examples/sharding/ShardingTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/sharding/TableCodesDynamicSqlSupport.java b/src/test/java/examples/sharding/TableCodesDynamicSqlSupport.java
index 00d64d0e1..6cec6e9c8 100644
--- a/src/test/java/examples/sharding/TableCodesDynamicSqlSupport.java
+++ b/src/test/java/examples/sharding/TableCodesDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/sharding/package-info.java b/src/test/java/examples/sharding/package-info.java
new file mode 100644
index 000000000..8c1f71235
--- /dev/null
+++ b/src/test/java/examples/sharding/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 examples.sharding;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/src/test/java/examples/simple/AddressDynamicSqlSupport.java b/src/test/java/examples/simple/AddressDynamicSqlSupport.java
index 14eec218b..be0581e31 100644
--- a/src/test/java/examples/simple/AddressDynamicSqlSupport.java
+++ b/src/test/java/examples/simple/AddressDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/simple/AddressMapper.java b/src/test/java/examples/simple/AddressMapper.java
index 49df34aeb..534162520 100644
--- a/src/test/java/examples/simple/AddressMapper.java
+++ b/src/test/java/examples/simple/AddressMapper.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/simple/AddressRecord.java b/src/test/java/examples/simple/AddressRecord.java
index f256ca1bc..5af471126 100644
--- a/src/test/java/examples/simple/AddressRecord.java
+++ b/src/test/java/examples/simple/AddressRecord.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/simple/CompoundKeyDynamicSqlSupport.java b/src/test/java/examples/simple/CompoundKeyDynamicSqlSupport.java
index c4e0c328d..88132049b 100644
--- a/src/test/java/examples/simple/CompoundKeyDynamicSqlSupport.java
+++ b/src/test/java/examples/simple/CompoundKeyDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/simple/CompoundKeyMapper.java b/src/test/java/examples/simple/CompoundKeyMapper.java
index d42d3336e..8f802860f 100644
--- a/src/test/java/examples/simple/CompoundKeyMapper.java
+++ b/src/test/java/examples/simple/CompoundKeyMapper.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/simple/CompoundKeyRow.java b/src/test/java/examples/simple/CompoundKeyRow.java
index fb47ca9d5..dc65b005a 100644
--- a/src/test/java/examples/simple/CompoundKeyRow.java
+++ b/src/test/java/examples/simple/CompoundKeyRow.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,23 +15,4 @@
  */
 package examples.simple;
 
-public class CompoundKeyRow {
-    private Integer id1;
-    private Integer id2;
-
-    public Integer getId1() {
-        return id1;
-    }
-
-    public void setId1(Integer id1) {
-        this.id1 = id1;
-    }
-
-    public Integer getId2() {
-        return id2;
-    }
-
-    public void setId2(Integer id2) {
-        this.id2 = id2;
-    }
-}
+public record CompoundKeyRow (Integer id1, Integer id2) {}
diff --git a/src/test/java/examples/simple/LastName.java b/src/test/java/examples/simple/LastName.java
index f909d90df..2a2499e14 100644
--- a/src/test/java/examples/simple/LastName.java
+++ b/src/test/java/examples/simple/LastName.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,42 +15,4 @@
  */
 package examples.simple;
 
-public class LastName {
-    private String name;
-
-    public String getName() {
-        return name;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
-
-    public static LastName of(String name) {
-        LastName lastName = new LastName();
-        lastName.setName(name);
-        return lastName;
-    }
-
-    @Override
-    public int hashCode() {
-        final int prime = 31;
-        int result = 1;
-        result = prime * result + ((name == null) ? 0 : name.hashCode());
-        return result;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj)
-            return true;
-        if (obj == null)
-            return false;
-        if (getClass() != obj.getClass())
-            return false;
-        LastName other = (LastName) obj;
-        if (name == null) {
-            return other.name == null;
-        } else return name.equals(other.name);
-    }
-}
+public record LastName (String name) {}
diff --git a/src/test/java/examples/simple/LastNameTypeHandler.java b/src/test/java/examples/simple/LastNameTypeHandler.java
index b9fa7cce0..de14fcc0d 100644
--- a/src/test/java/examples/simple/LastNameTypeHandler.java
+++ b/src/test/java/examples/simple/LastNameTypeHandler.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -22,12 +22,17 @@
 
 import org.apache.ibatis.type.JdbcType;
 import org.apache.ibatis.type.TypeHandler;
+import org.jspecify.annotations.Nullable;
 
 public class LastNameTypeHandler implements TypeHandler<LastName> {
 
     @Override
-    public void setParameter(PreparedStatement ps, int i, LastName parameter, JdbcType jdbcType) throws SQLException {
-        ps.setString(i, parameter == null ? null : parameter.getName());
+    public void setParameter(PreparedStatement ps, int i, @Nullable LastName parameter, JdbcType jdbcType) throws SQLException {
+        if (parameter == null) {
+            ps.setNull(i, jdbcType.TYPE_CODE);
+        } else {
+            ps.setString(i, parameter.name());
+        }
     }
 
     @Override
@@ -45,7 +50,7 @@ public LastName getResult(CallableStatement cs, int columnIndex) throws SQLExcep
         return toLastName(cs.getString(columnIndex));
     }
 
-    private LastName toLastName(String s) {
-        return s == null ? null : LastName.of(s);
+    private @Nullable LastName toLastName(@Nullable String s) {
+        return s == null ? null : new LastName(s);
     }
 }
diff --git a/src/test/java/examples/simple/MyBatisMapToRowTest.java b/src/test/java/examples/simple/MyBatisMapToRowTest.java
index f17bca807..7750195e9 100644
--- a/src/test/java/examples/simple/MyBatisMapToRowTest.java
+++ b/src/test/java/examples/simple/MyBatisMapToRowTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -28,9 +28,9 @@
 import java.io.InputStreamReader;
 import java.sql.Connection;
 import java.sql.DriverManager;
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Function;
 import java.util.stream.IntStream;
 
 import org.apache.ibatis.datasource.unpooled.UnpooledDataSource;
@@ -99,7 +99,7 @@ void testInsertOne() {
                     .orderBy(id1, id2)
                     .build().render(RenderingStrategies.MYBATIS3);
 
-            List<CompoundKeyRow> records = mapper.selectMany(selectStatement, this::mapRow);
+            List<CompoundKeyRow> records = mapper.selectMany(selectStatement, rowMapper);
             assertThat(records).hasSize(1);
         }
     }
@@ -109,10 +109,7 @@ void testInsertMultiple() {
         try (SqlSession session = sqlSessionFactory.openSession()) {
             CompoundKeyMapper mapper = session.getMapper(CompoundKeyMapper.class);
 
-            List<Integer> integers = new ArrayList<>();
-            integers.add(1);
-            integers.add(2);
-            integers.add(3);
+            List<Integer> integers = List.of(1, 2, 3);
 
             MultiRowInsertStatementProvider<Integer> insertStatement = insertMultiple(integers)
                     .into(compoundKey)
@@ -132,7 +129,7 @@ void testInsertMultiple() {
                     .orderBy(id1, id2)
                     .build().render(RenderingStrategies.MYBATIS3);
 
-            List<CompoundKeyRow> records = mapper.selectMany(selectStatement, this::mapRow);
+            List<CompoundKeyRow> records = mapper.selectMany(selectStatement, rowMapper);
             assertThat(records).hasSize(3);
         }
     }
@@ -142,10 +139,7 @@ void testInsertBatch() {
         try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
             CompoundKeyMapper mapper = session.getMapper(CompoundKeyMapper.class);
 
-            List<Integer> integers = new ArrayList<>();
-            integers.add(1);
-            integers.add(2);
-            integers.add(3);
+            List<Integer> integers = List.of(1, 2, 3);
 
             BatchInsert<Integer> insertStatement = insertBatch(integers)
                     .into(compoundKey)
@@ -172,15 +166,11 @@ void testInsertBatch() {
                     .orderBy(id1, id2)
                     .build().render(RenderingStrategies.MYBATIS3);
 
-            List<CompoundKeyRow> records = mapper.selectMany(selectStatement, this::mapRow);
+            List<CompoundKeyRow> records = mapper.selectMany(selectStatement, rowMapper);
             assertThat(records).hasSize(3);
         }
     }
 
-    private CompoundKeyRow mapRow(Map<String, Object> map) {
-        CompoundKeyRow answer = new CompoundKeyRow();
-        answer.setId1((Integer) map.get("ID1"));
-        answer.setId2((Integer) map.get("ID2"));
-        return answer;
-    }
+    private final Function<Map<String, Object>, CompoundKeyRow> rowMapper =
+            m -> new CompoundKeyRow((Integer) m.get("ID1"), (Integer) m.get("ID2"));
 }
diff --git a/src/test/java/examples/simple/PersonDynamicSqlSupport.java b/src/test/java/examples/simple/PersonDynamicSqlSupport.java
index 51b761be4..215f8a909 100644
--- a/src/test/java/examples/simple/PersonDynamicSqlSupport.java
+++ b/src/test/java/examples/simple/PersonDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/simple/PersonMapper.java b/src/test/java/examples/simple/PersonMapper.java
index bb9560f75..819b72c3b 100644
--- a/src/test/java/examples/simple/PersonMapper.java
+++ b/src/test/java/examples/simple/PersonMapper.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,17 +17,17 @@
 
 import static examples.simple.PersonDynamicSqlSupport.*;
 import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo;
+import static org.mybatis.dynamic.sql.SqlBuilder.isEqualToWhenPresent;
 
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Date;
 import java.util.List;
 import java.util.Optional;
 import java.util.function.UnaryOperator;
 
+import org.apache.ibatis.annotations.Arg;
 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.type.JdbcType;
 import org.mybatis.dynamic.sql.BasicColumn;
@@ -56,19 +56,23 @@
 public interface PersonMapper extends CommonCountMapper, CommonDeleteMapper, CommonInsertMapper<PersonRecord>, 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)
-    })
+    @Arg(column="A_ID", jdbcType=JdbcType.INTEGER, id=true, javaType = Integer.class)
+    @Arg(column="first_name", jdbcType=JdbcType.VARCHAR, javaType = String.class)
+    @Arg(column="last_name", jdbcType=JdbcType.VARCHAR, typeHandler=LastNameTypeHandler.class, javaType = LastName.class)
+    @Arg(column="birth_date", jdbcType=JdbcType.DATE, javaType = Date.class)
+    @Arg(column="employed", jdbcType=JdbcType.VARCHAR, typeHandler=YesNoTypeHandler.class, javaType = Boolean.class)
+    @Arg(column="occupation", jdbcType=JdbcType.VARCHAR, javaType = String.class)
+    @Arg(column="address_id", jdbcType=JdbcType.INTEGER, javaType = Integer.class)
     List<PersonRecord> selectMany(SelectStatementProvider selectStatement);
 
     @SelectProvider(type=SqlProviderAdapter.class, method="select")
-    @ResultMap("PersonResult")
+    @Arg(column="A_ID", jdbcType=JdbcType.INTEGER, id=true, javaType = Integer.class)
+    @Arg(column="first_name", jdbcType=JdbcType.VARCHAR, javaType = String.class)
+    @Arg(column="last_name", jdbcType=JdbcType.VARCHAR, typeHandler=LastNameTypeHandler.class, javaType = LastName.class)
+    @Arg(column="birth_date", jdbcType=JdbcType.DATE, javaType = Date.class)
+    @Arg(column="employed", jdbcType=JdbcType.VARCHAR, typeHandler=YesNoTypeHandler.class, javaType = Boolean.class)
+    @Arg(column="occupation", jdbcType=JdbcType.VARCHAR, javaType = String.class)
+    @Arg(column="address_id", jdbcType=JdbcType.INTEGER, javaType = Integer.class)
     Optional<PersonRecord> selectOne(SelectStatementProvider selectStatement);
 
     BasicColumn[] selectList =
@@ -90,9 +94,9 @@ default int delete(DeleteDSLCompleter completer) {
         return MyBatis3Utils.deleteFrom(this::delete, person, completer);
     }
 
-    default int deleteByPrimaryKey(Integer id_) {
+    default int deleteByPrimaryKey(Integer recordId) {
         return delete(c ->
-            c.where(id, isEqualTo(id_))
+            c.where(id, isEqualTo(recordId))
         );
     }
 
@@ -130,13 +134,13 @@ default int insertMultiple(Collection<PersonRecord> records) {
 
     default int insertSelective(PersonRecord row) {
         return MyBatis3Utils.insert(this::insert, row, person, c ->
-            c.map(id).toPropertyWhenPresent("id", row::getId)
-            .map(firstName).toPropertyWhenPresent("firstName", row::getFirstName)
-            .map(lastName).toPropertyWhenPresent("lastName", row::getLastName)
-            .map(birthDate).toPropertyWhenPresent("birthDate", row::getBirthDate)
-            .map(employed).toPropertyWhenPresent("employed", row::getEmployed)
-            .map(occupation).toPropertyWhenPresent("occupation", row::getOccupation)
-            .map(addressId).toPropertyWhenPresent("addressId", row::getAddressId)
+            c.map(id).toPropertyWhenPresent("id", row::id)
+            .map(firstName).toPropertyWhenPresent("firstName", row::firstName)
+            .map(lastName).toPropertyWhenPresent("lastName", row::lastName)
+            .map(birthDate).toPropertyWhenPresent("birthDate", row::birthDate)
+            .map(employed).toPropertyWhenPresent("employed", row::employed)
+            .map(occupation).toPropertyWhenPresent("occupation", row::occupation)
+            .map(addressId).toPropertyWhenPresent("addressId", row::addressId)
         );
     }
 
@@ -152,9 +156,9 @@ default List<PersonRecord> selectDistinct(SelectDSLCompleter completer) {
         return MyBatis3Utils.selectDistinct(this::selectMany, selectList, person, completer);
     }
 
-    default Optional<PersonRecord> selectByPrimaryKey(Integer id_) {
+    default Optional<PersonRecord> selectByPrimaryKey(Integer recordId) {
         return selectOne(c ->
-            c.where(id, isEqualTo(id_))
+            c.where(id, isEqualTo(recordId))
         );
     }
 
@@ -164,47 +168,47 @@ default int update(UpdateDSLCompleter completer) {
 
     static UpdateDSL<UpdateModel> updateAllColumns(PersonRecord row,
             UpdateDSL<UpdateModel> dsl) {
-        return dsl.set(id).equalTo(row::getId)
-                .set(firstName).equalTo(row::getFirstName)
-                .set(lastName).equalTo(row::getLastName)
-                .set(birthDate).equalTo(row::getBirthDate)
-                .set(employed).equalTo(row::getEmployed)
-                .set(occupation).equalTo(row::getOccupation)
-                .set(addressId).equalTo(row::getAddressId);
+        return dsl.set(id).equalToOrNull(row::id)
+                .set(firstName).equalToOrNull(row::firstName)
+                .set(lastName).equalToOrNull(row::lastName)
+                .set(birthDate).equalToOrNull(row::birthDate)
+                .set(employed).equalToOrNull(row::employed)
+                .set(occupation).equalToOrNull(row::occupation)
+                .set(addressId).equalToOrNull(row::addressId);
     }
 
     static UpdateDSL<UpdateModel> updateSelectiveColumns(PersonRecord row,
             UpdateDSL<UpdateModel> dsl) {
-        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)
-                .set(addressId).equalToWhenPresent(row::getAddressId);
+        return dsl.set(id).equalToWhenPresent(row::id)
+                .set(firstName).equalToWhenPresent(row::firstName)
+                .set(lastName).equalToWhenPresent(row::lastName)
+                .set(birthDate).equalToWhenPresent(row::birthDate)
+                .set(employed).equalToWhenPresent(row::employed)
+                .set(occupation).equalToWhenPresent(row::occupation)
+                .set(addressId).equalToWhenPresent(row::addressId);
     }
 
     default int updateByPrimaryKey(PersonRecord row) {
         return update(c ->
-            c.set(firstName).equalTo(row::getFirstName)
-            .set(lastName).equalTo(row::getLastName)
-            .set(birthDate).equalTo(row::getBirthDate)
-            .set(employed).equalTo(row::getEmployed)
-            .set(occupation).equalTo(row::getOccupation)
-            .set(addressId).equalTo(row::getAddressId)
-            .where(id, isEqualTo(row::getId))
+            c.set(firstName).equalToOrNull(row::firstName)
+            .set(lastName).equalToOrNull(row::lastName)
+            .set(birthDate).equalToOrNull(row::birthDate)
+            .set(employed).equalToOrNull(row::employed)
+            .set(occupation).equalToOrNull(row::occupation)
+            .set(addressId).equalToOrNull(row::addressId)
+            .where(id, isEqualToWhenPresent(row::id))
         );
     }
 
     default int updateByPrimaryKeySelective(PersonRecord row) {
         return update(c ->
-            c.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)
-            .set(addressId).equalToWhenPresent(row::getAddressId)
-            .where(id, isEqualTo(row::getId))
+            c.set(firstName).equalToWhenPresent(row::firstName)
+            .set(lastName).equalToWhenPresent(row::lastName)
+            .set(birthDate).equalToWhenPresent(row::birthDate)
+            .set(employed).equalToWhenPresent(row::employed)
+            .set(occupation).equalToWhenPresent(row::occupation)
+            .set(addressId).equalToWhenPresent(row::addressId)
+            .where(id, isEqualToWhenPresent(row::id))
         );
     }
 }
diff --git a/src/test/java/examples/simple/PersonMapperTest.java b/src/test/java/examples/simple/PersonMapperTest.java
index 05c47cd29..b02d52c4c 100644
--- a/src/test/java/examples/simple/PersonMapperTest.java
+++ b/src/test/java/examples/simple/PersonMapperTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -25,6 +25,7 @@
 import static examples.simple.PersonDynamicSqlSupport.occupation;
 import static examples.simple.PersonDynamicSqlSupport.person;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 import static org.assertj.core.api.Assertions.entry;
 import static org.mybatis.dynamic.sql.SqlBuilder.*;
 
@@ -32,11 +33,9 @@
 import java.io.InputStreamReader;
 import java.sql.Connection;
 import java.sql.DriverManager;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Date;
 import java.util.List;
-import java.util.Objects;
 import java.util.Optional;
 
 import org.apache.ibatis.datasource.unpooled.UnpooledDataSource;
@@ -52,6 +51,7 @@
 import org.mybatis.dynamic.sql.SortSpecification;
 import org.mybatis.dynamic.sql.delete.DeleteDSLCompleter;
 import org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider;
+import org.mybatis.dynamic.sql.exception.NonRenderingWhereClauseException;
 import org.mybatis.dynamic.sql.insert.render.GeneralInsertStatementProvider;
 import org.mybatis.dynamic.sql.render.RenderingStrategies;
 import org.mybatis.dynamic.sql.select.CountDSLCompleter;
@@ -109,7 +109,7 @@ void testSelectEmployed() {
                     .orderBy(id));
 
             assertThat(rows).hasSize(4);
-            assertThat(rows.get(0).getId()).isEqualTo(1);
+            assertThat(rows.get(0).id()).isEqualTo(1);
         }
     }
 
@@ -123,7 +123,7 @@ void testSelectUnemployed() {
                             .orderBy(id));
 
             assertThat(rows).hasSize(2);
-            assertThat(rows.get(0).getId()).isEqualTo(3);
+            assertThat(rows.get(0).id()).isEqualTo(3);
         }
     }
 
@@ -146,7 +146,7 @@ void testSelectWithTypeConversionAndFilterAndNull() {
             PersonMapper mapper = session.getMapper(PersonMapper.class);
 
             List<PersonRecord> rows = mapper.select(c ->
-                    c.where(id, isEqualTo((String) null).filter(Objects::nonNull).map(Integer::parseInt))
+                    c.where(id, isEqualToWhenPresent((String) null).map(Integer::parseInt))
                             .or(occupation, isNull()));
 
             assertThat(rows).hasSize(2);
@@ -180,8 +180,8 @@ void testSelectAll() {
             List<PersonRecord> rows = mapper.select(SelectDSLCompleter.allRows());
 
             assertThat(rows).hasSize(6);
-            assertThat(rows.get(0).getId()).isEqualTo(1);
-            assertThat(rows.get(5).getId()).isEqualTo(6);
+            assertThat(rows.get(0).id()).isEqualTo(1);
+            assertThat(rows.get(5).id()).isEqualTo(6);
         }
     }
 
@@ -194,8 +194,8 @@ void testSelectAllOrdered() {
                     .select(SelectDSLCompleter.allRowsOrderedBy(lastName.descending(), firstName.descending()));
 
             assertThat(rows).hasSize(6);
-            assertThat(rows.get(0).getId()).isEqualTo(5);
-            assertThat(rows.get(5).getId()).isEqualTo(1);
+            assertThat(rows.get(0).id()).isEqualTo(5);
+            assertThat(rows.get(5).id()).isEqualTo(1);
         }
     }
 
@@ -222,8 +222,8 @@ void testSelectWithTypeHandler() {
                     .orderBy(id));
 
             assertThat(rows).hasSize(2);
-            assertThat(rows.get(0).getId()).isEqualTo(3);
-            assertThat(rows.get(1).getId()).isEqualTo(6);
+            assertThat(rows.get(0).id()).isEqualTo(3);
+            assertThat(rows.get(1).id()).isEqualTo(6);
         }
     }
 
@@ -246,15 +246,20 @@ void testFirstNameIn() {
                     c.where(firstName, isIn("Fred", "Barney")));
 
             assertThat(rows).hasSize(2);
-            assertThat(rows.get(0).getLastName().getName()).isEqualTo("Flintstone");
-            assertThat(rows.get(1).getLastName().getName()).isEqualTo("Rubble");
+            assertThat(rows.get(0))
+                    .isNotNull()
+                    .extracting("lastName").isNotNull()
+                    .extracting("name").isEqualTo("Flintstone");
+            assertThat(rows.get(1))
+                    .isNotNull()
+                    .extracting("lastName").isNotNull()
+                    .extracting("name").isEqualTo("Rubble");
         }
     }
 
     @Test
     void testOrderByCollection() {
-        Collection<SortSpecification> orderByColumns = new ArrayList<>();
-        orderByColumns.add(firstName);
+        Collection<SortSpecification> orderByColumns = List.of(firstName);
 
         try (SqlSession session = sqlSessionFactory.openSession()) {
             PersonMapper mapper = session.getMapper(PersonMapper.class);
@@ -265,8 +270,14 @@ void testOrderByCollection() {
             );
 
             assertThat(rows).hasSize(2);
-            assertThat(rows.get(0).getLastName().getName()).isEqualTo("Rubble");
-            assertThat(rows.get(1).getLastName().getName()).isEqualTo("Flintstone");
+            assertThat(rows.get(0))
+                    .isNotNull()
+                    .extracting("lastName").isNotNull()
+                    .extracting("name").isEqualTo("Rubble");
+            assertThat(rows.get(1))
+                    .isNotNull()
+                    .extracting("lastName").isNotNull()
+                    .extracting("name").isEqualTo("Flintstone");
         }
     }
 
@@ -320,14 +331,7 @@ void testDeleteByPrimaryKey() {
     void testInsert() {
         try (SqlSession session = sqlSessionFactory.openSession()) {
             PersonMapper mapper = session.getMapper(PersonMapper.class);
-            PersonRecord row = new PersonRecord();
-            row.setId(100);
-            row.setFirstName("Joe");
-            row.setLastName(LastName.of("Jones"));
-            row.setBirthDate(new Date());
-            row.setEmployed(true);
-            row.setOccupation("Developer");
-            row.setAddressId(1);
+            PersonRecord row = new PersonRecord(100, "Joe", new LastName("Jones"), new Date(), true, "Developer", 1);
 
             int rows = mapper.insert(row);
             assertThat(rows).isEqualTo(1);
@@ -341,7 +345,7 @@ void testGeneralInsert() {
             int rows = mapper.generalInsert(c ->
                 c.set(id).toValue(100)
                 .set(firstName).toValue("Joe")
-                .set(lastName).toValue(LastName.of("Jones"))
+                .set(lastName).toValue(new LastName("Jones"))
                 .set(birthDate).toValue(new Date())
                 .set(employed).toValue(true)
                 .set(occupation).toValue("Developer")
@@ -357,27 +361,10 @@ void testInsertMultiple() {
         try (SqlSession session = sqlSessionFactory.openSession()) {
             PersonMapper mapper = session.getMapper(PersonMapper.class);
 
-            List<PersonRecord> records = new ArrayList<>();
-
-            PersonRecord row = new PersonRecord();
-            row.setId(100);
-            row.setFirstName("Joe");
-            row.setLastName(LastName.of("Jones"));
-            row.setBirthDate(new Date());
-            row.setEmployed(true);
-            row.setOccupation("Developer");
-            row.setAddressId(1);
-            records.add(row);
-
-            row = new PersonRecord();
-            row.setId(101);
-            row.setFirstName("Sarah");
-            row.setLastName(LastName.of("Smith"));
-            row.setBirthDate(new Date());
-            row.setEmployed(true);
-            row.setOccupation("Architect");
-            row.setAddressId(2);
-            records.add(row);
+            List<PersonRecord> records = List.of(
+                    new PersonRecord(100, "Joe", new LastName("Jones"), new Date(), true, "Developer", 1),
+                    new PersonRecord(101, "Sarah", new LastName("Smith"), new Date(), true, "Architect", 2)
+            );
 
             int rows = mapper.insertMultiple(records);
             assertThat(rows).isEqualTo(2);
@@ -388,42 +375,39 @@ void testInsertMultiple() {
     void testInsertSelective() {
         try (SqlSession session = sqlSessionFactory.openSession()) {
             PersonMapper mapper = session.getMapper(PersonMapper.class);
-            PersonRecord row = new PersonRecord();
-            row.setId(100);
-            row.setFirstName("Joe");
-            row.setLastName(LastName.of("Jones"));
-            row.setBirthDate(new Date());
-            row.setEmployed(false);
-            row.setAddressId(1);
+            PersonRecord row = new PersonRecord(100, "Joe", new LastName("Jones"), new Date(), false, null, 1);
 
             int rows = mapper.insertSelective(row);
             assertThat(rows).isEqualTo(1);
         }
     }
 
+    @Test
+    void testUpdateByPrimaryKeyNullKeyShouldThrowException() {
+        try (SqlSession session = sqlSessionFactory.openSession()) {
+            PersonMapper mapper = session.getMapper(PersonMapper.class);
+            PersonRecord row = new PersonRecord(null, "Joe", new LastName("Jones"), new Date(), true, "Developer", 1);
+
+            assertThatExceptionOfType(NonRenderingWhereClauseException.class).isThrownBy(() -> mapper.updateByPrimaryKey(row));
+        }
+    }
+
     @Test
     void testUpdateByPrimaryKey() {
         try (SqlSession session = sqlSessionFactory.openSession()) {
             PersonMapper mapper = session.getMapper(PersonMapper.class);
-            PersonRecord row = new PersonRecord();
-            row.setId(100);
-            row.setFirstName("Joe");
-            row.setLastName(LastName.of("Jones"));
-            row.setBirthDate(new Date());
-            row.setEmployed(true);
-            row.setOccupation("Developer");
-            row.setAddressId(1);
+            PersonRecord row = new PersonRecord(100, "Joe", new LastName("Jones"), new Date(), true, "Developer", 1);
 
             int rows = mapper.insert(row);
             assertThat(rows).isEqualTo(1);
 
-            row.setOccupation("Programmer");
+            row = row.withOccupation("Programmer");
             rows = mapper.updateByPrimaryKey(row);
             assertThat(rows).isEqualTo(1);
 
             Optional<PersonRecord> newRecord = mapper.selectByPrimaryKey(100);
             assertThat(newRecord).hasValueSatisfying(r ->
-                    assertThat(r.getOccupation()).isEqualTo("Programmer"));
+                    assertThat(r.occupation()).isEqualTo("Programmer"));
         }
     }
 
@@ -431,28 +415,19 @@ void testUpdateByPrimaryKey() {
     void testUpdateByPrimaryKeySelective() {
         try (SqlSession session = sqlSessionFactory.openSession()) {
             PersonMapper mapper = session.getMapper(PersonMapper.class);
-            PersonRecord row = new PersonRecord();
-            row.setId(100);
-            row.setFirstName("Joe");
-            row.setLastName(LastName.of("Jones"));
-            row.setBirthDate(new Date());
-            row.setEmployed(true);
-            row.setOccupation("Developer");
-            row.setAddressId(1);
+            PersonRecord row = new PersonRecord(100, "Joe", new LastName("Jones"), new Date(), true, "Developer", 1);
 
             int rows = mapper.insert(row);
             assertThat(rows).isEqualTo(1);
 
-            PersonRecord updateRecord = new PersonRecord();
-            updateRecord.setId(100);
-            updateRecord.setOccupation("Programmer");
+            PersonRecord updateRecord = new PersonRecord(100, null, null, null, null, "Programmer", null);
             rows = mapper.updateByPrimaryKeySelective(updateRecord);
             assertThat(rows).isEqualTo(1);
 
             Optional<PersonRecord> newRecord = mapper.selectByPrimaryKey(100);
             assertThat(newRecord).hasValueSatisfying(r -> {
-                assertThat(r.getOccupation()).isEqualTo("Programmer");
-                assertThat(r.getFirstName()).isEqualTo("Joe");
+                assertThat(r.occupation()).isEqualTo("Programmer");
+                assertThat(r.firstName()).isEqualTo("Joe");
             });
         }
     }
@@ -461,22 +436,15 @@ void testUpdateByPrimaryKeySelective() {
     void testUpdate() {
         try (SqlSession session = sqlSessionFactory.openSession()) {
             PersonMapper mapper = session.getMapper(PersonMapper.class);
-            PersonRecord row = new PersonRecord();
-            row.setId(100);
-            row.setFirstName("Joe");
-            row.setLastName(LastName.of("Jones"));
-            row.setBirthDate(new Date());
-            row.setEmployed(true);
-            row.setOccupation("Developer");
-            row.setAddressId(1);
+            PersonRecord row = new PersonRecord(100, "Joe", new LastName("Jones"), new Date(), true, "Developer", 1);
 
             int rows = mapper.insert(row);
             assertThat(rows).isEqualTo(1);
 
-            row.setOccupation("Programmer");
+            PersonRecord updateRow = row.withOccupation("Programmer");
 
             rows = mapper.update(c ->
-                PersonMapper.updateAllColumns(row, c)
+                PersonMapper.updateAllColumns(updateRow, c)
                 .where(id, isEqualTo(100))
                 .and(firstName, isEqualTo("Joe")));
 
@@ -484,7 +452,7 @@ void testUpdate() {
 
             Optional<PersonRecord> newRecord = mapper.selectByPrimaryKey(100);
             assertThat(newRecord).hasValueSatisfying(r ->
-                    assertThat(r.getOccupation()).isEqualTo("Programmer"));
+                    assertThat(r.occupation()).isEqualTo("Programmer"));
         }
     }
 
@@ -492,14 +460,7 @@ void testUpdate() {
     void testUpdateOneField() {
         try (SqlSession session = sqlSessionFactory.openSession()) {
             PersonMapper mapper = session.getMapper(PersonMapper.class);
-            PersonRecord row = new PersonRecord();
-            row.setId(100);
-            row.setFirstName("Joe");
-            row.setLastName(LastName.of("Jones"));
-            row.setBirthDate(new Date());
-            row.setEmployed(true);
-            row.setOccupation("Developer");
-            row.setAddressId(1);
+            PersonRecord row = new PersonRecord(100, "Joe", new LastName("Jones"), new Date(), true, "Developer", 1);
 
             int rows = mapper.insert(row);
             assertThat(rows).isEqualTo(1);
@@ -512,7 +473,7 @@ void testUpdateOneField() {
 
             Optional<PersonRecord> newRecord = mapper.selectByPrimaryKey(100);
             assertThat(newRecord).hasValueSatisfying(r ->
-                    assertThat(r.getOccupation()).isEqualTo("Programmer"));
+                    assertThat(r.occupation()).isEqualTo("Programmer"));
         }
     }
 
@@ -520,20 +481,12 @@ void testUpdateOneField() {
     void testUpdateAll() {
         try (SqlSession session = sqlSessionFactory.openSession()) {
             PersonMapper mapper = session.getMapper(PersonMapper.class);
-            PersonRecord row = new PersonRecord();
-            row.setId(100);
-            row.setFirstName("Joe");
-            row.setLastName(LastName.of("Jones"));
-            row.setBirthDate(new Date());
-            row.setEmployed(true);
-            row.setOccupation("Developer");
-            row.setAddressId(1);
+            PersonRecord row = new PersonRecord(100, "Joe", new LastName("Jones"), new Date(), true, "Developer", 1);
 
             int rows = mapper.insert(row);
             assertThat(rows).isEqualTo(1);
 
-            PersonRecord updateRecord = new PersonRecord();
-            updateRecord.setOccupation("Programmer");
+            PersonRecord updateRecord = new PersonRecord(null, null, null, null, null, "Programmer", null);
             rows = mapper.update(c ->
                 PersonMapper.updateSelectiveColumns(updateRecord, c));
 
@@ -541,7 +494,7 @@ void testUpdateAll() {
 
             Optional<PersonRecord> newRecord = mapper.selectByPrimaryKey(100);
             assertThat(newRecord).hasValueSatisfying(r ->
-                    assertThat(r.getOccupation()).isEqualTo("Programmer"));
+                    assertThat(r.occupation()).isEqualTo("Programmer"));
         }
     }
 
@@ -549,20 +502,12 @@ void testUpdateAll() {
     void testUpdateSelective() {
         try (SqlSession session = sqlSessionFactory.openSession()) {
             PersonMapper mapper = session.getMapper(PersonMapper.class);
-            PersonRecord row = new PersonRecord();
-            row.setId(100);
-            row.setFirstName("Joe");
-            row.setLastName(LastName.of("Jones"));
-            row.setBirthDate(new Date());
-            row.setEmployed(true);
-            row.setOccupation("Developer");
-            row.setAddressId(1);
+            PersonRecord row = new PersonRecord(100, "Joe", new LastName("Jones"), new Date(), true, "Developer", 1);
 
             int rows = mapper.insert(row);
             assertThat(rows).isEqualTo(1);
 
-            PersonRecord updateRecord = new PersonRecord();
-            updateRecord.setOccupation("Programmer");
+            PersonRecord updateRecord = new PersonRecord(null, null, null, null, null, "Programmer", null);
             rows = mapper.update(c ->
                 PersonMapper.updateSelectiveColumns(updateRecord, c)
                 .where(id, isEqualTo(100)));
@@ -571,7 +516,7 @@ void testUpdateSelective() {
 
             Optional<PersonRecord> newRecord = mapper.selectByPrimaryKey(100);
             assertThat(newRecord).hasValueSatisfying(r ->
-                    assertThat(r.getOccupation()).isEqualTo("Programmer"));
+                    assertThat(r.occupation()).isEqualTo("Programmer"));
         }
     }
 
@@ -622,11 +567,11 @@ void testTypeHandledLike() {
             PersonMapper mapper = session.getMapper(PersonMapper.class);
 
             List<PersonRecord> rows = mapper.select(c ->
-                    c.where(lastName, isLike(LastName.of("Fl%")))
+                    c.where(lastName, isLike(new LastName("Fl%")))
                     .orderBy(id));
 
             assertThat(rows).hasSize(3);
-            assertThat(rows.get(0).getFirstName()).isEqualTo("Fred");
+            assertThat(rows.get(0).firstName()).isEqualTo("Fred");
         }
     }
 
@@ -636,11 +581,11 @@ void testTypeHandledNotLike() {
             PersonMapper mapper = session.getMapper(PersonMapper.class);
 
             List<PersonRecord> rows = mapper.select(c ->
-                    c.where(lastName, isNotLike(LastName.of("Fl%")))
+                    c.where(lastName, isNotLike(new LastName("Fl%")))
                     .orderBy(id));
 
             assertThat(rows).hasSize(3);
-            assertThat(rows.get(0).getFirstName()).isEqualTo("Barney");
+            assertThat(rows.get(0).firstName()).isEqualTo("Barney");
         }
     }
 
@@ -656,7 +601,7 @@ void testJoinAllRows() {
             assertThat(records.get(0).getId()).isEqualTo(1);
             assertThat(records.get(0).getEmployed()).isTrue();
             assertThat(records.get(0).getFirstName()).isEqualTo("Fred");
-            assertThat(records.get(0).getLastName()).isEqualTo(LastName.of("Flintstone"));
+            assertThat(records.get(0).getLastName()).isEqualTo(new LastName("Flintstone"));
             assertThat(records.get(0).getOccupation()).isEqualTo("Brontosaurus Operator");
             assertThat(records.get(0).getBirthDate()).isNotNull();
             assertThat(records.get(0).getAddress().getId()).isEqualTo(1);
@@ -679,7 +624,7 @@ void testJoinOneRow() {
             assertThat(records.get(0).getId()).isEqualTo(1);
             assertThat(records.get(0).getEmployed()).isTrue();
             assertThat(records.get(0).getFirstName()).isEqualTo("Fred");
-            assertThat(records.get(0).getLastName()).isEqualTo(LastName.of("Flintstone"));
+            assertThat(records.get(0).getLastName()).isEqualTo(new LastName("Flintstone"));
             assertThat(records.get(0).getOccupation()).isEqualTo("Brontosaurus Operator");
             assertThat(records.get(0).getBirthDate()).isNotNull();
             assertThat(records.get(0).getAddress().getId()).isEqualTo(1);
@@ -699,7 +644,7 @@ void testJoinPrimaryKey() {
                 assertThat(r.getId()).isEqualTo(1);
                 assertThat(r.getEmployed()).isTrue();
                 assertThat(r.getFirstName()).isEqualTo("Fred");
-                assertThat(r.getLastName()).isEqualTo(LastName.of("Flintstone"));
+                assertThat(r.getLastName()).isEqualTo(new LastName("Flintstone"));
                 assertThat(r.getOccupation()).isEqualTo("Brontosaurus Operator");
                 assertThat(r.getBirthDate()).isNotNull();
                 assertThat(r.getAddress().getId()).isEqualTo(1);
@@ -809,8 +754,8 @@ void testMultiSelectWithUnion() {
             List<PersonRecord> records = mapper.selectMany(selectStatement);
 
             assertThat(records).hasSize(2);
-            assertThat(records.get(0).getId()).isEqualTo(1);
-            assertThat(records.get(1).getId()).isEqualTo(6);
+            assertThat(records.get(0).id()).isEqualTo(1);
+            assertThat(records.get(1).id()).isEqualTo(6);
         }
     }
 
@@ -854,8 +799,8 @@ void testMultiSelectWithUnionAll() {
             List<PersonRecord> records = mapper.selectMany(selectStatement);
 
             assertThat(records).hasSize(2);
-            assertThat(records.get(0).getId()).isEqualTo(1);
-            assertThat(records.get(1).getId()).isEqualTo(6);
+            assertThat(records.get(0).id()).isEqualTo(1);
+            assertThat(records.get(1).id()).isEqualTo(6);
         }
     }
 
diff --git a/src/test/java/examples/simple/PersonRecord.java b/src/test/java/examples/simple/PersonRecord.java
index 57ac78bd9..10a9d9edf 100644
--- a/src/test/java/examples/simple/PersonRecord.java
+++ b/src/test/java/examples/simple/PersonRecord.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,70 +15,14 @@
  */
 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;
-
-    public Integer getId() {
-        return id;
-    }
-
-    public void setId(Integer id) {
-        this.id = id;
-    }
-
-    public String getFirstName() {
-        return firstName;
-    }
-
-    public void setFirstName(String firstName) {
-        this.firstName = firstName;
-    }
-
-    public LastName getLastName() {
-        return lastName;
-    }
-
-    public void setLastName(LastName lastName) {
-        this.lastName = lastName;
-    }
+import org.jspecify.annotations.Nullable;
 
-    public Date getBirthDate() {
-        return birthDate;
-    }
-
-    public void setBirthDate(Date birthDate) {
-        this.birthDate = birthDate;
-    }
-
-    public String getOccupation() {
-        return occupation;
-    }
-
-    public void setOccupation(String occupation) {
-        this.occupation = occupation;
-    }
-
-    public Boolean getEmployed() {
-        return employed;
-    }
-
-    public void setEmployed(Boolean employed) {
-        this.employed = employed;
-    }
-
-    public Integer getAddressId() {
-        return addressId;
-    }
+import java.util.Date;
 
-    public void setAddressId(Integer addressId) {
-        this.addressId = addressId;
+public record PersonRecord (@Nullable Integer id, @Nullable String firstName, @Nullable LastName lastName,
+                            @Nullable Date birthDate, @Nullable Boolean employed, @Nullable String occupation,
+                            @Nullable Integer addressId) {
+    public PersonRecord withOccupation(String occupation) {
+        return new PersonRecord(id, firstName, lastName, birthDate, employed, occupation, addressId);
     }
 }
diff --git a/src/test/java/examples/simple/PersonWithAddress.java b/src/test/java/examples/simple/PersonWithAddress.java
index a7a07b37f..ec38997e5 100644
--- a/src/test/java/examples/simple/PersonWithAddress.java
+++ b/src/test/java/examples/simple/PersonWithAddress.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/simple/PersonWithAddressMapper.java b/src/test/java/examples/simple/PersonWithAddressMapper.java
index a36946bad..55607a8f7 100644
--- a/src/test/java/examples/simple/PersonWithAddressMapper.java
+++ b/src/test/java/examples/simple/PersonWithAddressMapper.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -80,25 +80,25 @@ public interface PersonWithAddressMapper extends CommonCountMapper {
 
     default Optional<PersonWithAddress> selectOne(SelectDSLCompleter completer) {
         QueryExpressionDSL<SelectModel> start = SqlBuilder.select(selectList).from(person)
-                .join(address, on(person.addressId, equalTo(address.id)));
+                .join(address, on(person.addressId, isEqualTo(address.id)));
         return MyBatis3Utils.selectOne(this::selectOne, start, completer);
     }
 
     default List<PersonWithAddress> select(SelectDSLCompleter completer) {
         QueryExpressionDSL<SelectModel> start = SqlBuilder.select(selectList).from(person)
-                .join(address, on(person.addressId, equalTo(address.id)));
+                .join(address, on(person.addressId, isEqualTo(address.id)));
         return MyBatis3Utils.selectList(this::selectMany, start, completer);
     }
 
-    default Optional<PersonWithAddress> selectByPrimaryKey(Integer id_) {
+    default Optional<PersonWithAddress> selectByPrimaryKey(Integer recordId) {
         return selectOne(c ->
-            c.where(id, isEqualTo(id_))
+            c.where(id, isEqualTo(recordId))
         );
     }
 
     default long count(CountDSLCompleter completer) {
         CountDSL<SelectModel> start = countFrom(person)
-                .join(address, on(person.addressId, equalTo(address.id)));
+                .join(address, on(person.addressId, isEqualTo(address.id)));
         return MyBatis3Utils.countFrom(this::count, start, completer);
     }
 }
diff --git a/src/test/java/examples/simple/ReusableWhereTest.java b/src/test/java/examples/simple/ReusableWhereTest.java
index a0ca639d3..f3b6d1d4e 100644
--- a/src/test/java/examples/simple/ReusableWhereTest.java
+++ b/src/test/java/examples/simple/ReusableWhereTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/simple/YesNoTypeHandler.java b/src/test/java/examples/simple/YesNoTypeHandler.java
index 780538d4e..a721ccb1e 100644
--- a/src/test/java/examples/simple/YesNoTypeHandler.java
+++ b/src/test/java/examples/simple/YesNoTypeHandler.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/simple/package-info.java b/src/test/java/examples/simple/package-info.java
new file mode 100644
index 000000000..79a124103
--- /dev/null
+++ b/src/test/java/examples/simple/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 examples.simple;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/src/test/java/examples/spring/AddressDynamicSqlSupport.java b/src/test/java/examples/spring/AddressDynamicSqlSupport.java
index ec0233fa9..9e68a0c0f 100644
--- a/src/test/java/examples/spring/AddressDynamicSqlSupport.java
+++ b/src/test/java/examples/spring/AddressDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/spring/AddressRecord.java b/src/test/java/examples/spring/AddressRecord.java
index e691e33d8..3987285ab 100644
--- a/src/test/java/examples/spring/AddressRecord.java
+++ b/src/test/java/examples/spring/AddressRecord.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,41 +15,4 @@
  */
 package examples.spring;
 
-public class AddressRecord {
-    private Integer id;
-    private String streetAddress;
-    private String city;
-    private String state;
-
-    public Integer getId() {
-        return id;
-    }
-
-    public void setId(Integer id) {
-        this.id = id;
-    }
-
-    public String getStreetAddress() {
-        return streetAddress;
-    }
-
-    public void setStreetAddress(String streetAddress) {
-        this.streetAddress = streetAddress;
-    }
-
-    public String getCity() {
-        return city;
-    }
-
-    public void setCity(String city) {
-        this.city = city;
-    }
-
-    public String getState() {
-        return state;
-    }
-
-    public void setState(String state) {
-        this.state = state;
-    }
-}
+public record AddressRecord (Integer id, String streetAddress, String city, String state) { }
diff --git a/src/test/java/examples/spring/CompoundKeyDynamicSqlSupport.java b/src/test/java/examples/spring/CompoundKeyDynamicSqlSupport.java
index cc02ed756..af8021eb2 100644
--- a/src/test/java/examples/spring/CompoundKeyDynamicSqlSupport.java
+++ b/src/test/java/examples/spring/CompoundKeyDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/spring/CompoundKeyRow.java b/src/test/java/examples/spring/CompoundKeyRow.java
index d2f00d53e..d9422b447 100644
--- a/src/test/java/examples/spring/CompoundKeyRow.java
+++ b/src/test/java/examples/spring/CompoundKeyRow.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,23 +15,4 @@
  */
 package examples.spring;
 
-public class CompoundKeyRow {
-    private Integer id1;
-    private Integer id2;
-
-    public Integer getId1() {
-        return id1;
-    }
-
-    public void setId1(Integer id1) {
-        this.id1 = id1;
-    }
-
-    public Integer getId2() {
-        return id2;
-    }
-
-    public void setId2(Integer id2) {
-        this.id2 = id2;
-    }
-}
+public record CompoundKeyRow (Integer id1, Integer id2) {}
diff --git a/src/test/java/examples/spring/LastName.java b/src/test/java/examples/spring/LastName.java
index 389ad04c3..e6e9d93dd 100644
--- a/src/test/java/examples/spring/LastName.java
+++ b/src/test/java/examples/spring/LastName.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,42 +15,6 @@
  */
 package examples.spring;
 
-public class LastName {
-    private String name;
+import org.jspecify.annotations.Nullable;
 
-    public String getName() {
-        return name;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
-
-    public static LastName of(String name) {
-        LastName lastName = new LastName();
-        lastName.setName(name);
-        return lastName;
-    }
-
-    @Override
-    public int hashCode() {
-        final int prime = 31;
-        int result = 1;
-        result = prime * result + ((name == null) ? 0 : name.hashCode());
-        return result;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj)
-            return true;
-        if (obj == null)
-            return false;
-        if (getClass() != obj.getClass())
-            return false;
-        LastName other = (LastName) obj;
-        if (name == null) {
-            return other.name == null;
-        } else return name.equals(other.name);
-    }
-}
+public record LastName(@Nullable String name) { }
diff --git a/src/test/java/examples/spring/LastNameParameterConverter.java b/src/test/java/examples/spring/LastNameParameterConverter.java
index db59773c8..413aea7cb 100644
--- a/src/test/java/examples/spring/LastNameParameterConverter.java
+++ b/src/test/java/examples/spring/LastNameParameterConverter.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,12 +15,18 @@
  */
 package examples.spring;
 
+import org.jspecify.annotations.Nullable;
 import org.mybatis.dynamic.sql.ParameterTypeConverter;
 import org.springframework.core.convert.converter.Converter;
 
-public class LastNameParameterConverter implements ParameterTypeConverter<LastName, String>, Converter<LastName, String> {
+public class LastNameParameterConverter implements ParameterTypeConverter<LastName, String>,
+        Converter<LastName, String> {
     @Override
-    public String convert(LastName source) {
-        return source == null ? null : source.getName();
+    public @Nullable String convert(LastName source) {
+        if ("Slate".equals(source.name())) {
+            return null;
+        } else {
+            return source.name();
+        }
     }
 }
diff --git a/src/test/java/examples/spring/PersonDynamicSqlSupport.java b/src/test/java/examples/spring/PersonDynamicSqlSupport.java
index efb5103c6..ea61e6703 100644
--- a/src/test/java/examples/spring/PersonDynamicSqlSupport.java
+++ b/src/test/java/examples/spring/PersonDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/spring/PersonRecord.java b/src/test/java/examples/spring/PersonRecord.java
index d7a81883f..73417e5c6 100644
--- a/src/test/java/examples/spring/PersonRecord.java
+++ b/src/test/java/examples/spring/PersonRecord.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -15,78 +15,23 @@
  */
 package examples.spring;
 
-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;
-
-    public Integer getId() {
-        return id;
-    }
-
-    public void setId(Integer id) {
-        this.id = id;
-    }
-
-    public String getFirstName() {
-        return firstName;
-    }
-
-    public void setFirstName(String firstName) {
-        this.firstName = firstName;
-    }
-
-    public LastName getLastName() {
-        return lastName;
-    }
-
-    public String getLastNameAsString() {
-        return lastName == null ? null : lastName.getName();
-    }
+import org.jspecify.annotations.Nullable;
 
-    public void setLastName(LastName lastName) {
-        this.lastName = lastName;
-    }
-
-    public Date getBirthDate() {
-        return birthDate;
-    }
-
-    public void setBirthDate(Date birthDate) {
-        this.birthDate = birthDate;
-    }
-
-    public String getOccupation() {
-        return occupation;
-    }
+import java.util.Date;
 
-    public void setOccupation(String occupation) {
-        this.occupation = occupation;
-    }
+public record PersonRecord (Integer id, @Nullable String firstName, @Nullable LastName lastName,
+                            @Nullable Date birthDate, @Nullable Boolean employed,
+                            @Nullable String occupation, @Nullable Integer addressId) {
 
-    public Boolean getEmployed() {
-        return employed;
+    public @Nullable String getLastNameAsString() {
+        return lastName == null ? null : lastName.name();
     }
 
     public String getEmployedAsString() {
         return employed == null ? "No" : employed ? "Yes" : "No";
     }
 
-    public void setEmployed(Boolean employed) {
-        this.employed = employed;
-    }
-
-    public Integer getAddressId() {
-        return addressId;
-    }
-
-    public void setAddressId(Integer addressId) {
-        this.addressId = addressId;
+    public PersonRecord withOccupation(String occupation) {
+        return new PersonRecord(id, firstName, lastName, birthDate, employed, occupation, addressId);
     }
 }
diff --git a/src/test/java/examples/spring/PersonTemplateTest.java b/src/test/java/examples/spring/PersonTemplateTest.java
index 2cd2b6b42..b92c03a50 100644
--- a/src/test/java/examples/spring/PersonTemplateTest.java
+++ b/src/test/java/examples/spring/PersonTemplateTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -20,7 +20,6 @@
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mybatis.dynamic.sql.SqlBuilder.*;
 
-import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
 import java.util.Optional;
@@ -31,12 +30,14 @@
 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.GeneralInsertStatementProvider;
+import org.mybatis.dynamic.sql.render.RenderingStrategies;
 import org.mybatis.dynamic.sql.select.SelectModel;
 import org.mybatis.dynamic.sql.update.UpdateModel;
 import org.mybatis.dynamic.sql.util.Buildable;
 import org.mybatis.dynamic.sql.util.spring.NamedParameterJdbcTemplateExtensions;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.jdbc.core.BeanPropertyRowMapper;
+import org.springframework.jdbc.core.DataClassRowMapper;
 import org.springframework.jdbc.core.RowMapper;
 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
 import org.springframework.transaction.annotation.Transactional;
@@ -68,8 +69,8 @@ void testSelectAll() {
         List<PersonRecord> rows = template.selectList(selectStatement, personRowMapper);
 
         assertThat(rows).hasSize(6);
-        assertThat(rows.get(0).getId()).isEqualTo(1);
-        assertThat(rows.get(5).getId()).isEqualTo(6);
+        assertThat(rows.get(0).id()).isEqualTo(1);
+        assertThat(rows.get(5).id()).isEqualTo(6);
 
     }
 
@@ -82,8 +83,8 @@ void testSelectAllOrdered() {
         List<PersonRecord> rows = template.selectList(selectStatement, personRowMapper);
 
         assertThat(rows).hasSize(6);
-        assertThat(rows.get(0).getId()).isEqualTo(5);
-        assertThat(rows.get(5).getId()).isEqualTo(1);
+        assertThat(rows.get(0).id()).isEqualTo(5);
+        assertThat(rows.get(5).id()).isEqualTo(1);
 
     }
 
@@ -149,36 +150,36 @@ void testSelectWithTypeHandler() {
         List<PersonRecord> rows = template.selectList(selectStatement, personRowMapper);
 
         assertThat(rows).hasSize(2);
-        assertThat(rows.get(0).getId()).isEqualTo(3);
-        assertThat(rows.get(1).getId()).isEqualTo(6);
+        assertThat(rows.get(0).id()).isEqualTo(3);
+        assertThat(rows.get(1).id()).isEqualTo(6);
     }
 
     @Test
     void testSelectBetweenWithTypeHandler() {
         Buildable<SelectModel> selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId)
                 .from(person)
-                .where(lastName, isBetween(LastName.of("Adams")).and(LastName.of("Jones")))
+                .where(lastName, isBetween(new LastName("Adams")).and(new LastName("Jones")))
                 .orderBy(id);
 
         List<PersonRecord> rows = template.selectList(selectStatement, personRowMapper);
 
         assertThat(rows).hasSize(3);
-        assertThat(rows.get(0).getId()).isEqualTo(1);
-        assertThat(rows.get(1).getId()).isEqualTo(2);
+        assertThat(rows.get(0).id()).isEqualTo(1);
+        assertThat(rows.get(1).id()).isEqualTo(2);
     }
 
     @Test
     void testSelectListWithTypeHandler() {
         Buildable<SelectModel> selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId)
                 .from(person)
-                .where(lastName, isIn(LastName.of("Flintstone"), LastName.of("Rubble")))
+                .where(lastName, isIn(new LastName("Flintstone"), new LastName("Rubble")))
                 .orderBy(id);
 
         List<PersonRecord> rows = template.selectList(selectStatement, personRowMapper);
 
         assertThat(rows).hasSize(6);
-        assertThat(rows.get(0).getId()).isEqualTo(1);
-        assertThat(rows.get(1).getId()).isEqualTo(2);
+        assertThat(rows.get(0).id()).isEqualTo(1);
+        assertThat(rows.get(1).id()).isEqualTo(2);
     }
 
     @Test
@@ -201,8 +202,15 @@ void testFirstNameIn() {
         List<PersonRecord> rows = template.selectList(selectStatement, personRowMapper);
 
         assertThat(rows).hasSize(2);
-        assertThat(rows.get(0).getLastName().getName()).isEqualTo("Flintstone");
-        assertThat(rows.get(1).getLastName().getName()).isEqualTo("Rubble");
+
+        assertThat(rows).satisfiesExactly(
+                person1 -> assertThat(person1).isNotNull()
+                        .extracting("lastName").isNotNull()
+                        .extracting("name").isEqualTo("Flintstone"),
+                person2 -> assertThat(person2).isNotNull()
+                        .extracting("lastName").isNotNull()
+                        .extracting("name").isEqualTo("Rubble")
+        );
     }
 
     @Test
@@ -236,14 +244,7 @@ void testDeleteByPrimaryKey() {
 
     @Test
     void testInsert() {
-        PersonRecord row = new PersonRecord();
-        row.setId(100);
-        row.setFirstName("Joe");
-        row.setLastName(LastName.of("Jones"));
-        row.setBirthDate(new Date());
-        row.setEmployed(true);
-        row.setOccupation("Developer");
-        row.setAddressId(1);
+        PersonRecord row = new PersonRecord(100, "Joe", new LastName("Jones"), new Date(), true, "Developer", 1);
 
         Buildable<InsertModel<PersonRecord>> insertStatement = insert(row).into(person)
                 .map(id).toProperty("id")
@@ -264,7 +265,7 @@ void testGeneralInsert() {
         Buildable<GeneralInsertModel> insertStatement = insertInto(person)
                 .set(id).toValue(100)
                 .set(firstName).toValue("Joe")
-                .set(lastName).toValue(LastName.of("Jones"))
+                .set(lastName).toValue(new LastName("Jones"))
                 .set(birthDate).toValue(new Date())
                 .set(employed).toValue(true)
                 .set(occupation).toValue("Developer")
@@ -278,27 +279,9 @@ void testGeneralInsert() {
     @Test
     void testInsertMultiple() {
 
-        List<PersonRecord> records = new ArrayList<>();
-
-        PersonRecord row = new PersonRecord();
-        row.setId(100);
-        row.setFirstName("Joe");
-        row.setLastName(LastName.of("Jones"));
-        row.setBirthDate(new Date());
-        row.setEmployed(true);
-        row.setOccupation("Developer");
-        row.setAddressId(1);
-        records.add(row);
-
-        row = new PersonRecord();
-        row.setId(101);
-        row.setFirstName("Sarah");
-        row.setLastName(LastName.of("Smith"));
-        row.setBirthDate(new Date());
-        row.setEmployed(true);
-        row.setOccupation("Architect");
-        row.setAddressId(2);
-        records.add(row);
+        List<PersonRecord> records = List.of(
+                new PersonRecord(100, "Joe", new LastName("Jones"), new Date(), true, "Developer", 1),
+                new PersonRecord(101, "Sarah", new LastName("Smith"), new Date(), true, "Architect", 2));
 
         Buildable<MultiRowInsertModel<PersonRecord>> insertStatement = insertMultiple(records).into(person)
                 .map(id).toProperty("id")
@@ -317,27 +300,9 @@ void testInsertMultiple() {
     @Test
     void testInsertBatch() {
 
-        List<PersonRecord> records = new ArrayList<>();
-
-        PersonRecord row = new PersonRecord();
-        row.setId(100);
-        row.setFirstName("Joe");
-        row.setLastName(LastName.of("Jones"));
-        row.setBirthDate(new Date());
-        row.setEmployed(true);
-        row.setOccupation("Developer");
-        row.setAddressId(1);
-        records.add(row);
-
-        row = new PersonRecord();
-        row.setId(101);
-        row.setFirstName("Sarah");
-        row.setLastName(LastName.of("Smith"));
-        row.setBirthDate(new Date());
-        row.setEmployed(true);
-        row.setOccupation("Architect");
-        row.setAddressId(2);
-        records.add(row);
+        List<PersonRecord> records = List.of(
+                new PersonRecord(100, "Joe", new LastName("Jones"), new Date(), true, "Developer", 1),
+                new PersonRecord(101, "Sarah", new LastName("Smith"), new Date(), true, "Architect", 2));
 
         Buildable<BatchInsertModel<PersonRecord>> insertStatement = insertBatch(records).into(person)
                 .map(id).toProperty("id")
@@ -357,35 +322,57 @@ void testInsertBatch() {
 
     @Test
     void testInsertSelective() {
-        PersonRecord row = new PersonRecord();
-        row.setId(100);
-        row.setFirstName("Joe");
-        row.setLastName(LastName.of("Jones"));
-        row.setBirthDate(new Date());
-        row.setEmployed(false);
-        row.setAddressId(1);
+        PersonRecord row = new PersonRecord(100, "Joe", new LastName("Jones"), new Date(), false, null, 1);
 
         Buildable<InsertModel<PersonRecord>> insertStatement = insert(row).into(person)
-                .map(id).toPropertyWhenPresent("id", row::getId)
-                .map(firstName).toPropertyWhenPresent("firstName", row::getFirstName)
+                .map(id).toPropertyWhenPresent("id", row::id)
+                .map(firstName).toPropertyWhenPresent("firstName", row::firstName)
                 .map(lastName).toPropertyWhenPresent("lastNameAsString", row::getLastNameAsString)
-                .map(birthDate).toPropertyWhenPresent("birthDate", row::getBirthDate)
+                .map(birthDate).toPropertyWhenPresent("birthDate", row::birthDate)
                 .map(employed).toPropertyWhenPresent("employedAsString", row::getEmployedAsString)
-                .map(occupation).toPropertyWhenPresent("occupation", row::getOccupation)
-                .map(addressId).toPropertyWhenPresent("addressId", row::getAddressId);
+                .map(occupation).toPropertyWhenPresent("occupation", row::occupation)
+                .map(addressId).toPropertyWhenPresent("addressId", row::addressId);
 
         int rows = template.insert(insertStatement);
 
         assertThat(rows).isEqualTo(1);
     }
 
+    @Test
+    void testGeneralInsertWhenTypeConverterReturnsNull() {
+
+        GeneralInsertStatementProvider insertStatement = insertInto(person)
+                .set(id).toValue(100)
+                .set(firstName).toValue("Joe")
+                .set(lastName).toValueWhenPresent(new LastName("Slate"))
+                .set(birthDate).toValue(new Date())
+                .set(employed).toValue(true)
+                .set(occupation).toValue("Quarry Owner")
+                .set(addressId).toValue(1)
+                .build()
+                .render(RenderingStrategies.SPRING_NAMED_PARAMETER);
+
+        assertThat(insertStatement.getInsertStatement())
+                .isEqualTo("insert into Person (id, first_name, birth_date, employed, occupation, address_id) values (:p1, :p2, :p3, :p4, :p5, :p6)");
+        int rows = template.generalInsert(insertStatement);
+        assertThat(rows).isEqualTo(1);
+
+        Buildable<SelectModel> selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId)
+                .from(person)
+                .where(id, isEqualTo(100));
+        Optional<PersonRecord> newRecord = template.selectOne(selectStatement, personRowMapper);
+        assertThat(newRecord).hasValueSatisfying(
+                r -> assertThat(r).isNotNull().extracting("lastName").isNotNull().extracting("name").isNull()
+        );
+    }
+
     @Test
     void testUpdateByPrimaryKey() {
 
         Buildable<GeneralInsertModel> insertStatement = insertInto(person)
                 .set(id).toValue(100)
                 .set(firstName).toValue("Joe")
-                .set(lastName).toValue(LastName.of("Jones"))
+                .set(lastName).toValue(new LastName("Jones"))
                 .set(birthDate).toValue(new Date())
                 .set(employed).toValue(true)
                 .set(occupation).toValue("Developer")
@@ -405,7 +392,7 @@ void testUpdateByPrimaryKey() {
                 .from(person)
                 .where(id, isEqualTo(100));
         Optional<PersonRecord> newRecord = template.selectOne(selectStatement, personRowMapper);
-        assertThat(newRecord).hasValueSatisfying(r -> assertThat(r.getOccupation()).isEqualTo("Programmer"));
+        assertThat(newRecord).hasValueSatisfying(r -> assertThat(r.occupation()).isEqualTo("Programmer"));
     }
 
     @Test
@@ -414,7 +401,7 @@ void testUpdateByPrimaryKeyWithTypeHandler() {
         Buildable<GeneralInsertModel> insertStatement = insertInto(person)
                 .set(id).toValue(100)
                 .set(firstName).toValue("Joe")
-                .set(lastName).toValue(LastName.of("Jones"))
+                .set(lastName).toValue(new LastName("Jones"))
                 .set(birthDate).toValue(new Date())
                 .set(employed).toValue(true)
                 .set(occupation).toValue("Developer")
@@ -424,7 +411,7 @@ void testUpdateByPrimaryKeyWithTypeHandler() {
         assertThat(rows).isEqualTo(1);
 
         Buildable<UpdateModel> updateStatement = update(person)
-                .set(lastName).equalTo(LastName.of("Smith"))
+                .set(lastName).equalTo(new LastName("Smith"))
                 .where(id, isEqualTo(100));
 
         rows = template.update(updateStatement);
@@ -434,8 +421,10 @@ void testUpdateByPrimaryKeyWithTypeHandler() {
                 .from(person)
                 .where(id, isEqualTo(100));
         Optional<PersonRecord> newRecord = template.selectOne(selectStatement, personRowMapper);
-        assertThat(newRecord).hasValueSatisfying(r ->
-                assertThat(r.getLastName().getName()).isEqualTo("Smith"));
+        assertThat(newRecord).hasValueSatisfying(r -> assertThat(r).isNotNull()
+                .extracting("lastName").isNotNull()
+                .extracting("name").isEqualTo("Smith")
+        );
     }
 
     @Test
@@ -443,7 +432,7 @@ void testUpdateByPrimaryKeySelective() {
         Buildable<GeneralInsertModel> insertStatement = insertInto(person)
                 .set(id).toValue(100)
                 .set(firstName).toValue("Joe")
-                .set(lastName).toValue(LastName.of("Jones"))
+                .set(lastName).toValue(new LastName("Jones"))
                 .set(birthDate).toValue(new Date())
                 .set(employed).toValue(true)
                 .set(occupation).toValue("Developer")
@@ -452,18 +441,16 @@ void testUpdateByPrimaryKeySelective() {
         int rows = template.generalInsert(insertStatement);
         assertThat(rows).isEqualTo(1);
 
-        PersonRecord updateRecord = new PersonRecord();
-        updateRecord.setId(100);
-        updateRecord.setOccupation("Programmer");
+        PersonRecord updateRecord = new PersonRecord(100, null, null, null, null, "Programmer", null);
 
         Buildable<UpdateModel> updateStatement = update(person)
-                .set(firstName).equalToWhenPresent(updateRecord::getFirstName)
-                .set(lastName).equalToWhenPresent(updateRecord::getLastName)
-                .set(birthDate).equalToWhenPresent(updateRecord::getBirthDate)
-                .set(employed).equalToWhenPresent(updateRecord::getEmployed)
-                .set(occupation).equalToWhenPresent(updateRecord::getOccupation)
-                .set(addressId).equalToWhenPresent(updateRecord::getAddressId)
-                .where(id, isEqualTo(updateRecord::getId));
+                .set(firstName).equalToWhenPresent(updateRecord::firstName)
+                .set(lastName).equalToWhenPresent(updateRecord::lastName)
+                .set(birthDate).equalToWhenPresent(updateRecord::birthDate)
+                .set(employed).equalToWhenPresent(updateRecord::employed)
+                .set(occupation).equalToWhenPresent(updateRecord::occupation)
+                .set(addressId).equalToWhenPresent(updateRecord::addressId)
+                .where(id, isEqualTo(updateRecord::id));
 
         rows = template.update(updateStatement);
         assertThat(rows).isEqualTo(1);
@@ -473,21 +460,14 @@ void testUpdateByPrimaryKeySelective() {
                 .where(id, isEqualTo(100));
         Optional<PersonRecord> newRecord = template.selectOne(selectStatement, personRowMapper);
         assertThat(newRecord).hasValueSatisfying(r -> {
-            assertThat(r.getOccupation()).isEqualTo("Programmer");
-            assertThat(r.getFirstName()).isEqualTo("Joe");
+            assertThat(r.occupation()).isEqualTo("Programmer");
+            assertThat(r.firstName()).isEqualTo("Joe");
         });
     }
 
     @Test
     void testUpdate() {
-        PersonRecord row = new PersonRecord();
-        row.setId(100);
-        row.setFirstName("Joe");
-        row.setLastName(LastName.of("Jones"));
-        row.setBirthDate(new Date());
-        row.setEmployed(true);
-        row.setOccupation("Developer");
-        row.setAddressId(1);
+        PersonRecord row = new PersonRecord(100, "Joe", new LastName("Jones"), new Date(), true, "Developer", 1);
 
         Buildable<InsertModel<PersonRecord>> insertStatement = insert(row).into(person)
                 .map(id).toProperty("id")
@@ -501,16 +481,16 @@ void testUpdate() {
         int rows = template.insert(insertStatement);
         assertThat(rows).isEqualTo(1);
 
-        row.setOccupation("Programmer");
+        row = row.withOccupation("Programmer");
 
         Buildable<UpdateModel> updateStatement = update(person)
-                .set(firstName).equalTo(row::getFirstName)
-                .set(lastName).equalTo(row::getLastName)
-                .set(birthDate).equalTo(row::getBirthDate)
-                .set(employed).equalTo(row::getEmployed)
-                .set(occupation).equalTo(row::getOccupation)
-                .set(addressId).equalTo(row::getAddressId)
-                .where(id, isEqualTo(row::getId));
+                .set(firstName).equalToWhenPresent(row::firstName)
+                .set(lastName).equalToWhenPresent(row::lastName)
+                .set(birthDate).equalToWhenPresent(row::birthDate)
+                .set(employed).equalToWhenPresent(row::employed)
+                .set(occupation).equalToWhenPresent(row::occupation)
+                .set(addressId).equalToWhenPresent(row::addressId)
+                .where(id, isEqualTo(row::id));
 
         rows = template.update(updateStatement);
         assertThat(rows).isEqualTo(1);
@@ -520,8 +500,8 @@ void testUpdate() {
                 .where(id, isEqualTo(100));
         Optional<PersonRecord> newRecord = template.selectOne(selectStatement, personRowMapper);
         assertThat(newRecord).hasValueSatisfying(r -> {
-            assertThat(r.getOccupation()).isEqualTo("Programmer");
-            assertThat(r.getFirstName()).isEqualTo("Joe");
+            assertThat(r.occupation()).isEqualTo("Programmer");
+            assertThat(r.firstName()).isEqualTo("Joe");
         });
     }
 
@@ -530,7 +510,7 @@ void testUpdateAll() {
         Buildable<GeneralInsertModel> insertStatement = insertInto(person)
                 .set(id).toValue(100)
                 .set(firstName).toValue("Joe")
-                .set(lastName).toValue(LastName.of("Jones"))
+                .set(lastName).toValue(new LastName("Jones"))
                 .set(birthDate).toValue(new Date())
                 .set(employed).toValue(true)
                 .set(occupation).toValue("Developer")
@@ -550,7 +530,7 @@ void testUpdateAll() {
                 .where(id, isEqualTo(100));
 
         Optional<PersonRecord> newRecord = template.selectOne(selectStatement, personRowMapper);
-        assertThat(newRecord).hasValueSatisfying(r -> assertThat(r.getOccupation()).isEqualTo("Programmer"));
+        assertThat(newRecord).hasValueSatisfying(r -> assertThat(r.occupation()).isEqualTo("Programmer"));
     }
 
     @Test
@@ -590,25 +570,25 @@ void testCountDistinctLastName() {
     void testTypeHandledLike() {
         Buildable<SelectModel> selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId)
                 .from(person)
-                .where(lastName, isLike(LastName.of("Fl%")))
+                .where(lastName, isLike(new LastName("Fl%")))
                 .orderBy(id);
 
         List<PersonRecord> rows = template.selectList(selectStatement, personRowMapper);
         assertThat(rows).hasSize(3);
-        assertThat(rows.get(0).getFirstName()).isEqualTo("Fred");
+        assertThat(rows.get(0).firstName()).isEqualTo("Fred");
     }
 
     @Test
     void testTypeHandledNotLike() {
         Buildable<SelectModel> selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId)
                 .from(person)
-                .where(lastName, isNotLike(LastName.of("Fl%")))
+                .where(lastName, isNotLike(new LastName("Fl%")))
                 .orderBy(id);
 
         List<PersonRecord> rows = template.selectList(selectStatement, personRowMapper);
 
         assertThat(rows).hasSize(3);
-        assertThat(rows.get(0).getFirstName()).isEqualTo("Barney");
+        assertThat(rows.get(0).firstName()).isEqualTo("Barney");
     }
 
     @Test
@@ -618,14 +598,15 @@ void testAutoMapping() {
                 .from(address)
                 .orderBy(address.id);
 
+
         List<AddressRecord> records = template.selectList(selectStatement,
-                BeanPropertyRowMapper.newInstance(AddressRecord.class));
+                DataClassRowMapper.newInstance(AddressRecord.class));
 
         assertThat(records).hasSize(2);
-        assertThat(records.get(0).getId()).isEqualTo(1);
-        assertThat(records.get(0).getStreetAddress()).isEqualTo("123 Main Street");
-        assertThat(records.get(0).getCity()).isEqualTo("Bedrock");
-        assertThat(records.get(0).getState()).isEqualTo("IN");
+        assertThat(records.get(0).id()).isEqualTo(1);
+        assertThat(records.get(0).streetAddress()).isEqualTo("123 Main Street");
+        assertThat(records.get(0).city()).isEqualTo("Bedrock");
+        assertThat(records.get(0).state()).isEqualTo("IN");
     }
 
     @Test
@@ -633,22 +614,22 @@ void testJoinAllRows() {
         Buildable<SelectModel> selectStatement = select(id, firstName, lastName, birthDate, employed, occupation,
                 address.id, address.streetAddress, address.city, address.state)
                 .from(person)
-                .join(address, on(person.addressId, equalTo(address.id)))
+                .join(address, on(person.addressId, isEqualTo(address.id)))
                 .orderBy(id);
 
         List<PersonWithAddress> records = template.selectList(selectStatement, personWithAddressRowMapper);
 
         assertThat(records).hasSize(6);
-        assertThat(records.get(0).getId()).isEqualTo(1);
-        assertThat(records.get(0).getEmployed()).isTrue();
-        assertThat(records.get(0).getFirstName()).isEqualTo("Fred");
-        assertThat(records.get(0).getLastName()).isEqualTo(LastName.of("Flintstone"));
-        assertThat(records.get(0).getOccupation()).isEqualTo("Brontosaurus Operator");
-        assertThat(records.get(0).getBirthDate()).isNotNull();
-        assertThat(records.get(0).getAddress().getId()).isEqualTo(1);
-        assertThat(records.get(0).getAddress().getStreetAddress()).isEqualTo("123 Main Street");
-        assertThat(records.get(0).getAddress().getCity()).isEqualTo("Bedrock");
-        assertThat(records.get(0).getAddress().getState()).isEqualTo("IN");
+        assertThat(records.get(0).id()).isEqualTo(1);
+        assertThat(records.get(0).employed()).isTrue();
+        assertThat(records.get(0).firstName()).isEqualTo("Fred");
+        assertThat(records.get(0).lastName()).isEqualTo(new LastName("Flintstone"));
+        assertThat(records.get(0).occupation()).isEqualTo("Brontosaurus Operator");
+        assertThat(records.get(0).birthDate()).isNotNull();
+        assertThat(records.get(0).address().id()).isEqualTo(1);
+        assertThat(records.get(0).address().streetAddress()).isEqualTo("123 Main Street");
+        assertThat(records.get(0).address().city()).isEqualTo("Bedrock");
+        assertThat(records.get(0).address().state()).isEqualTo("IN");
     }
 
     @Test
@@ -656,22 +637,22 @@ void testJoinOneRow() {
         Buildable<SelectModel> selectStatement = select(id, firstName, lastName, birthDate, employed, occupation,
                 address.id, address.streetAddress, address.city, address.state)
                 .from(person)
-                .join(address, on(person.addressId, equalTo(address.id)))
+                .join(address, on(person.addressId, isEqualTo(address.id)))
                 .where(id, isEqualTo(1));
 
         List<PersonWithAddress> records = template.selectList(selectStatement, personWithAddressRowMapper);
 
         assertThat(records).hasSize(1);
-        assertThat(records.get(0).getId()).isEqualTo(1);
-        assertThat(records.get(0).getEmployed()).isTrue();
-        assertThat(records.get(0).getFirstName()).isEqualTo("Fred");
-        assertThat(records.get(0).getLastName()).isEqualTo(LastName.of("Flintstone"));
-        assertThat(records.get(0).getOccupation()).isEqualTo("Brontosaurus Operator");
-        assertThat(records.get(0).getBirthDate()).isNotNull();
-        assertThat(records.get(0).getAddress().getId()).isEqualTo(1);
-        assertThat(records.get(0).getAddress().getStreetAddress()).isEqualTo("123 Main Street");
-        assertThat(records.get(0).getAddress().getCity()).isEqualTo("Bedrock");
-        assertThat(records.get(0).getAddress().getState()).isEqualTo("IN");
+        assertThat(records.get(0).id()).isEqualTo(1);
+        assertThat(records.get(0).employed()).isTrue();
+        assertThat(records.get(0).firstName()).isEqualTo("Fred");
+        assertThat(records.get(0).lastName()).isEqualTo(new LastName("Flintstone"));
+        assertThat(records.get(0).occupation()).isEqualTo("Brontosaurus Operator");
+        assertThat(records.get(0).birthDate()).isNotNull();
+        assertThat(records.get(0).address().id()).isEqualTo(1);
+        assertThat(records.get(0).address().streetAddress()).isEqualTo("123 Main Street");
+        assertThat(records.get(0).address().city()).isEqualTo("Bedrock");
+        assertThat(records.get(0).address().state()).isEqualTo("IN");
     }
 
     @Test
@@ -679,22 +660,22 @@ void testJoinPrimaryKey() {
         Buildable<SelectModel> selectStatement = select(id, firstName, lastName, birthDate, employed, occupation,
                 address.id, address.streetAddress, address.city, address.state)
                 .from(person)
-                .join(address, on(person.addressId, equalTo(address.id)))
+                .join(address, on(person.addressId, isEqualTo(address.id)))
                 .where(id, isEqualTo(1));
 
         Optional<PersonWithAddress> row = template.selectOne(selectStatement, personWithAddressRowMapper);
 
         assertThat(row).hasValueSatisfying(r -> {
-            assertThat(r.getId()).isEqualTo(1);
-            assertThat(r.getEmployed()).isTrue();
-            assertThat(r.getFirstName()).isEqualTo("Fred");
-            assertThat(r.getLastName()).isEqualTo(LastName.of("Flintstone"));
-            assertThat(r.getOccupation()).isEqualTo("Brontosaurus Operator");
-            assertThat(r.getBirthDate()).isNotNull();
-            assertThat(r.getAddress().getId()).isEqualTo(1);
-            assertThat(r.getAddress().getStreetAddress()).isEqualTo("123 Main Street");
-            assertThat(r.getAddress().getCity()).isEqualTo("Bedrock");
-            assertThat(r.getAddress().getState()).isEqualTo("IN");
+            assertThat(r.id()).isEqualTo(1);
+            assertThat(r.employed()).isTrue();
+            assertThat(r.firstName()).isEqualTo("Fred");
+            assertThat(r.lastName()).isEqualTo(new LastName("Flintstone"));
+            assertThat(r.occupation()).isEqualTo("Brontosaurus Operator");
+            assertThat(r.birthDate()).isNotNull();
+            assertThat(r.address().id()).isEqualTo(1);
+            assertThat(r.address().streetAddress()).isEqualTo("123 Main Street");
+            assertThat(r.address().city()).isEqualTo("Bedrock");
+            assertThat(r.address().state()).isEqualTo("IN");
         });
     }
 
@@ -703,7 +684,7 @@ void testJoinPrimaryKeyInvalidRecord() {
         Buildable<SelectModel> selectStatement = select(id, firstName, lastName, birthDate, employed, occupation,
                 address.id, address.streetAddress, address.city, address.state)
                 .from(person)
-                .join(address, on(person.addressId, equalTo(address.id)))
+                .join(address, on(person.addressId, isEqualTo(address.id)))
                 .where(id, isEqualTo(55));
 
         Optional<PersonWithAddress> row = template.selectOne(selectStatement, personWithAddressRowMapper);
@@ -713,7 +694,7 @@ void testJoinPrimaryKeyInvalidRecord() {
     @Test
     void testJoinCount() {
         Buildable<SelectModel> countStatement = countFrom(person)
-                .join(address, on(person.addressId, equalTo(address.id)))
+                .join(address, on(person.addressId, isEqualTo(address.id)))
                 .where(id, isEqualTo(55));
 
         long count = template.count(countStatement);
@@ -723,7 +704,7 @@ void testJoinCount() {
     @Test
     void testJoinCountWithSubCriteria() {
         Buildable<SelectModel> countStatement = countFrom(person)
-                .join(address, on(person.addressId, equalTo(address.id)))
+                .join(address, on(person.addressId, isEqualTo(address.id)))
                 .where(person.id, isEqualTo(55), or(person.id, isEqualTo(1)));
 
         long count = template.count(countStatement);
@@ -731,36 +712,24 @@ void testJoinCountWithSubCriteria() {
     }
 
     private final RowMapper<PersonWithAddress> personWithAddressRowMapper =
-            (rs, i) -> {
-                PersonWithAddress row = new PersonWithAddress();
-                row.setId(rs.getInt(1));
-                row.setFirstName(rs.getString(2));
-                row.setLastName(LastName.of(rs.getString(3)));
-                row.setBirthDate(rs.getTimestamp(4));
-                row.setEmployed("Yes".equals(rs.getString(5)));
-                row.setOccupation(rs.getString(6));
-
-                AddressRecord address = new AddressRecord();
-                row.setAddress(address);
-                address.setId(rs.getInt(7));
-                address.setStreetAddress(rs.getString(8));
-                address.setCity(rs.getString(9));
-                address.setState(rs.getString(10));
-
-                return row;
-            };
-
-
-    static RowMapper<PersonRecord> personRowMapper =
-            (rs, i) -> {
-                PersonRecord row = new PersonRecord();
-                row.setId(rs.getInt(1));
-                row.setFirstName(rs.getString(2));
-                row.setLastName(LastName.of(rs.getString(3)));
-                row.setBirthDate(rs.getTimestamp(4));
-                row.setEmployed("Yes".equals(rs.getString(5)));
-                row.setOccupation(rs.getString(6));
-                row.setAddressId(rs.getInt(7));
-                return row;
-            };
+            (rs, i) -> new PersonWithAddress(rs.getInt(1),
+                    rs.getString(2),
+                    new LastName(rs.getString(3)),
+                    rs.getTimestamp(4),
+                    "Yes".equals(rs.getString(5)),
+                    rs.getString(6),
+                    new AddressRecord(rs.getInt(7),
+                            rs.getString(8),
+                            rs.getString(9),
+                            rs.getString(10)));
+
+
+    static final RowMapper<PersonRecord> personRowMapper =
+            (rs, i) -> new PersonRecord(rs.getInt(1),
+                    rs.getString(2),
+                    new LastName(rs.getString(3)),
+                    rs.getTimestamp(4),
+                    "Yes".equals(rs.getString(5)),
+                    rs.getString(6),
+                    rs.getInt(7));
 }
diff --git a/src/test/java/examples/spring/PersonWithAddress.java b/src/test/java/examples/spring/PersonWithAddress.java
index e0e1269e1..7cf524553 100644
--- a/src/test/java/examples/spring/PersonWithAddress.java
+++ b/src/test/java/examples/spring/PersonWithAddress.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,68 +17,5 @@
 
 import java.util.Date;
 
-public class PersonWithAddress {
-    private Integer id;
-    private String firstName;
-    private LastName lastName;
-    private Date birthDate;
-    private Boolean employed;
-    private String occupation;
-    private AddressRecord address;
-
-    public Integer getId() {
-        return id;
-    }
-
-    public void setId(Integer id) {
-        this.id = id;
-    }
-
-    public String getFirstName() {
-        return firstName;
-    }
-
-    public void setFirstName(String firstName) {
-        this.firstName = firstName;
-    }
-
-    public LastName getLastName() {
-        return lastName;
-    }
-
-    public void setLastName(LastName lastName) {
-        this.lastName = lastName;
-    }
-
-    public Date getBirthDate() {
-        return birthDate;
-    }
-
-    public void setBirthDate(Date birthDate) {
-        this.birthDate = birthDate;
-    }
-
-    public String getOccupation() {
-        return occupation;
-    }
-
-    public void setOccupation(String occupation) {
-        this.occupation = occupation;
-    }
-
-    public Boolean getEmployed() {
-        return employed;
-    }
-
-    public void setEmployed(Boolean employed) {
-        this.employed = employed;
-    }
-
-    public AddressRecord getAddress() {
-        return address;
-    }
-
-    public void setAddress(AddressRecord address) {
-        this.address = address;
-    }
-}
+public record PersonWithAddress(Integer id, String firstName, LastName lastName, Date birthDate, Boolean employed,
+                                String occupation, AddressRecord address) { }
diff --git a/src/test/java/examples/spring/ReusableWhereTest.java b/src/test/java/examples/spring/ReusableWhereTest.java
index b626cf63e..821b27621 100644
--- a/src/test/java/examples/spring/ReusableWhereTest.java
+++ b/src/test/java/examples/spring/ReusableWhereTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/spring/SpringConfiguration.java b/src/test/java/examples/spring/SpringConfiguration.java
index edc7e4b65..f62bab71e 100644
--- a/src/test/java/examples/spring/SpringConfiguration.java
+++ b/src/test/java/examples/spring/SpringConfiguration.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/spring/SpringMapToRowTest.java b/src/test/java/examples/spring/SpringMapToRowTest.java
index 91271b39f..0a2d5fe61 100644
--- a/src/test/java/examples/spring/SpringMapToRowTest.java
+++ b/src/test/java/examples/spring/SpringMapToRowTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -24,7 +24,6 @@
 import static org.mybatis.dynamic.sql.SqlBuilder.insertMultiple;
 import static org.mybatis.dynamic.sql.SqlBuilder.select;
 
-import java.util.ArrayList;
 import java.util.List;
 import java.util.stream.IntStream;
 
@@ -74,10 +73,7 @@ void testInsertOne() {
 
     @Test
     void testInsertMultiple() {
-        List<Integer> integers = new ArrayList<>();
-        integers.add(1);
-        integers.add(2);
-        integers.add(3);
+        List<Integer> integers = List.of(1, 2, 3);
 
         MultiRowInsertStatementProvider<Integer> insertStatement = insertMultiple(integers)
                 .into(compoundKey)
@@ -102,10 +98,7 @@ void testInsertMultiple() {
 
     @Test
     void testInsertBatch() {
-        List<Integer> integers = new ArrayList<>();
-        integers.add(1);
-        integers.add(2);
-        integers.add(3);
+        List<Integer> integers = List.of(1, 2, 3);
 
         BatchInsert<Integer> insertStatement = insertBatch(integers)
                 .into(compoundKey)
@@ -129,11 +122,6 @@ void testInsertBatch() {
         assertThat(records).hasSize(3);
     }
 
-    static RowMapper<CompoundKeyRow> rowMapper =
-            (rs, i) -> {
-                CompoundKeyRow answer = new CompoundKeyRow();
-                answer.setId1(rs.getInt("ID1"));
-                answer.setId2(rs.getInt("ID2"));
-                return answer;
-            };
+    static final RowMapper<CompoundKeyRow> rowMapper =
+            (rs, i) -> new CompoundKeyRow(rs.getInt("ID1"), rs.getInt("ID2"));
 }
diff --git a/src/test/java/examples/spring/YesNoParameterConverter.java b/src/test/java/examples/spring/YesNoParameterConverter.java
index d2154a359..9801fe1ae 100644
--- a/src/test/java/examples/spring/YesNoParameterConverter.java
+++ b/src/test/java/examples/spring/YesNoParameterConverter.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -21,6 +21,6 @@ public class YesNoParameterConverter implements ParameterTypeConverter<Boolean,
 
     @Override
     public String convert(Boolean source) {
-        return source == null ? null : source ? "Yes" : "No";
+        return source ? "Yes" : "No";
     }
 }
diff --git a/src/test/java/examples/spring/package-info.java b/src/test/java/examples/spring/package-info.java
new file mode 100644
index 000000000..27fa1f118
--- /dev/null
+++ b/src/test/java/examples/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 examples.spring;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/src/test/java/examples/springbatch/SpringBatchRenderingTest.java b/src/test/java/examples/springbatch/SpringBatchRenderingTest.java
new file mode 100644
index 000000000..6cbd617cc
--- /dev/null
+++ b/src/test/java/examples/springbatch/SpringBatchRenderingTest.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 examples.springbatch;
+
+import static examples.springbatch.mapper.PersonDynamicSqlSupport.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mybatis.dynamic.sql.SqlBuilder.isLike;
+import static org.mybatis.dynamic.sql.SqlBuilder.select;
+
+import org.junit.jupiter.api.Test;
+import org.mybatis.dynamic.sql.util.springbatch.SpringBatchUtility;
+
+class SpringBatchRenderingTest {
+
+    @Test
+    void renderLimit() {
+        var selectStatement = select(person.allColumns())
+                .from(person)
+                .where(firstName, isLike("%f%"))
+                .limit(SpringBatchUtility.MYBATIS_SPRING_BATCH_PAGESIZE)
+                .offset(SpringBatchUtility.MYBATIS_SPRING_BATCH_SKIPROWS)
+                .build()
+                .render(SpringBatchUtility.SPRING_BATCH_PAGING_ITEM_READER_RENDERING_STRATEGY);
+
+        assertThat(selectStatement.getSelectStatement())
+                .isEqualTo("""
+                     select * \
+                     from person \
+                     where first_name like #{parameters.p1,jdbcType=VARCHAR} \
+                     limit #{_pagesize} \
+                     offset #{_skiprows}""");
+    }
+
+    @Test
+    void renderFetchFirst() {
+        var selectStatement = select(person.allColumns())
+                .from(person)
+                .where(firstName, isLike("%f%"))
+                .offset(SpringBatchUtility.MYBATIS_SPRING_BATCH_SKIPROWS)
+                .fetchFirst(SpringBatchUtility.MYBATIS_SPRING_BATCH_PAGESIZE).rowsOnly()
+                .build()
+                .render(SpringBatchUtility.SPRING_BATCH_PAGING_ITEM_READER_RENDERING_STRATEGY);
+
+        assertThat(selectStatement.getSelectStatement())
+                .isEqualTo("""
+                     select * \
+                     from person \
+                     where first_name like #{parameters.p1,jdbcType=VARCHAR} \
+                     offset #{_skiprows} rows \
+                     fetch first #{_pagesize} rows only""");
+    }
+}
diff --git a/src/test/java/examples/springbatch/bulkinsert/BulkInsertConfiguration.java b/src/test/java/examples/springbatch/bulkinsert/BulkInsertConfiguration.java
index 2e27d3e30..60278b02e 100644
--- a/src/test/java/examples/springbatch/bulkinsert/BulkInsertConfiguration.java
+++ b/src/test/java/examples/springbatch/bulkinsert/BulkInsertConfiguration.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/springbatch/bulkinsert/SpringBatchBulkInsertTest.java b/src/test/java/examples/springbatch/bulkinsert/SpringBatchBulkInsertTest.java
index 5b09844f8..a5cf3e2e3 100644
--- a/src/test/java/examples/springbatch/bulkinsert/SpringBatchBulkInsertTest.java
+++ b/src/test/java/examples/springbatch/bulkinsert/SpringBatchBulkInsertTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/springbatch/bulkinsert/TestRecordGenerator.java b/src/test/java/examples/springbatch/bulkinsert/TestRecordGenerator.java
index 0f1fd9ff2..023e7ee4d 100644
--- a/src/test/java/examples/springbatch/bulkinsert/TestRecordGenerator.java
+++ b/src/test/java/examples/springbatch/bulkinsert/TestRecordGenerator.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/springbatch/common/PersonProcessor.java b/src/test/java/examples/springbatch/common/PersonProcessor.java
index c0d6c165f..2a4939cdb 100644
--- a/src/test/java/examples/springbatch/common/PersonProcessor.java
+++ b/src/test/java/examples/springbatch/common/PersonProcessor.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -29,7 +29,7 @@ public class PersonProcessor implements ItemProcessor<PersonRecord, PersonRecord
     private ExecutionContext executionContext;
 
     @Override
-    public PersonRecord process(PersonRecord person) throws Exception {
+    public PersonRecord process(PersonRecord person) {
         incrementRowCount();
 
         PersonRecord transformed = new PersonRecord();
diff --git a/src/test/java/examples/springbatch/common/PersonRecord.java b/src/test/java/examples/springbatch/common/PersonRecord.java
index 31f9a227c..edf974f53 100644
--- a/src/test/java/examples/springbatch/common/PersonRecord.java
+++ b/src/test/java/examples/springbatch/common/PersonRecord.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/springbatch/common/UpdateStatementConvertor.java b/src/test/java/examples/springbatch/common/UpdateStatementConvertor.java
index d310f75bd..089486b71 100644
--- a/src/test/java/examples/springbatch/common/UpdateStatementConvertor.java
+++ b/src/test/java/examples/springbatch/common/UpdateStatementConvertor.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/springbatch/cursor/CursorReaderBatchConfiguration.java b/src/test/java/examples/springbatch/cursor/CursorReaderBatchConfiguration.java
index 823f1c631..164716cd2 100644
--- a/src/test/java/examples/springbatch/cursor/CursorReaderBatchConfiguration.java
+++ b/src/test/java/examples/springbatch/cursor/CursorReaderBatchConfiguration.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,10 +18,12 @@
 import static examples.springbatch.mapper.PersonDynamicSqlSupport.lastName;
 import static examples.springbatch.mapper.PersonDynamicSqlSupport.person;
 import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo;
+import static org.mybatis.dynamic.sql.SqlBuilder.select;
 
 import javax.sql.DataSource;
 
 import org.apache.ibatis.session.SqlSessionFactory;
+import org.mybatis.dynamic.sql.render.RenderingStrategies;
 import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
 import org.mybatis.dynamic.sql.update.render.UpdateStatementProvider;
 import org.mybatis.dynamic.sql.util.springbatch.SpringBatchUtility;
@@ -89,11 +91,11 @@ public PlatformTransactionManager transactionManager(DataSource dataSource) {
 
     @Bean
     public MyBatisCursorItemReader<PersonRecord> reader(SqlSessionFactory sqlSessionFactory) {
-        SelectStatementProvider selectStatement =  SpringBatchUtility.selectForCursor(person.allColumns())
+        SelectStatementProvider selectStatement =  select(person.allColumns())
                 .from(person)
                 .where(lastName, isEqualTo("flintstone"))
                 .build()
-                .render();
+                .render(RenderingStrategies.MYBATIS3);
 
         MyBatisCursorItemReader<PersonRecord> reader = new MyBatisCursorItemReader<>();
         reader.setQueryId(PersonMapper.class.getName() + ".selectMany");
diff --git a/src/test/java/examples/springbatch/cursor/SpringBatchCursorTest.java b/src/test/java/examples/springbatch/cursor/SpringBatchCursorTest.java
index 544a5b30c..747df2db0 100644
--- a/src/test/java/examples/springbatch/cursor/SpringBatchCursorTest.java
+++ b/src/test/java/examples/springbatch/cursor/SpringBatchCursorTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/springbatch/mapper/PersonDynamicSqlSupport.java b/src/test/java/examples/springbatch/mapper/PersonDynamicSqlSupport.java
index 227f6ee50..b5a264509 100644
--- a/src/test/java/examples/springbatch/mapper/PersonDynamicSqlSupport.java
+++ b/src/test/java/examples/springbatch/mapper/PersonDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -22,17 +22,17 @@
 
 public class PersonDynamicSqlSupport {
 
-    public static Person person = new Person();
-    public static SqlColumn<Integer> id = person.id;
-    public static SqlColumn<String> firstName = person.firstName;
-    public static SqlColumn<String> lastName = person.lastName;
-    public static SqlColumn<Boolean> forPagingTest = person.forPagingTest;
+    public static final Person person = new Person();
+    public static final SqlColumn<Integer> id = person.id;
+    public static final SqlColumn<String> firstName = person.firstName;
+    public static final SqlColumn<String> lastName = person.lastName;
+    public static final SqlColumn<Boolean> forPagingTest = person.forPagingTest;
 
     public static class Person extends SqlTable {
-        public SqlColumn<Integer> id = column("id", JDBCType.INTEGER);
-        public SqlColumn<String> firstName = column("first_name", JDBCType.VARCHAR);
-        public SqlColumn<String> lastName = column("last_name", JDBCType.VARCHAR);
-        public SqlColumn<Boolean> forPagingTest = column("for_paging_test", JDBCType.BOOLEAN);
+        public final SqlColumn<Integer> id = column("id", JDBCType.INTEGER);
+        public final SqlColumn<String> firstName = column("first_name", JDBCType.VARCHAR);
+        public final SqlColumn<String> lastName = column("last_name", JDBCType.VARCHAR);
+        public final SqlColumn<Boolean> forPagingTest = column("for_paging_test", JDBCType.BOOLEAN);
 
         public Person() {
             super("person");
diff --git a/src/test/java/examples/springbatch/mapper/PersonMapper.java b/src/test/java/examples/springbatch/mapper/PersonMapper.java
index 6fc3bea8f..a1db34626 100644
--- a/src/test/java/examples/springbatch/mapper/PersonMapper.java
+++ b/src/test/java/examples/springbatch/mapper/PersonMapper.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -20,7 +20,6 @@
 
 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.mybatis.dynamic.sql.util.mybatis3.CommonCountMapper;
 import org.mybatis.dynamic.sql.util.mybatis3.CommonInsertMapper;
@@ -33,10 +32,8 @@
 public interface PersonMapper extends CommonCountMapper, CommonInsertMapper<PersonRecord>, CommonUpdateMapper {
 
     @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")
-    })
+    @Result(column="id", property="id", id=true)
+    @Result(column="first_name", property="firstName")
+    @Result(column="last_name", property="lastName")
     List<PersonRecord> selectMany(Map<String, Object> parameterValues);
 }
diff --git a/src/test/java/examples/springbatch/paging/PagingReaderBatchConfiguration.java b/src/test/java/examples/springbatch/paging/PagingReaderBatchConfiguration.java
index 41353ab35..1da98bb9f 100644
--- a/src/test/java/examples/springbatch/paging/PagingReaderBatchConfiguration.java
+++ b/src/test/java/examples/springbatch/paging/PagingReaderBatchConfiguration.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,6 +17,7 @@
 
 import static examples.springbatch.mapper.PersonDynamicSqlSupport.*;
 import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo;
+import static org.mybatis.dynamic.sql.SqlBuilder.select;
 
 import javax.sql.DataSource;
 
@@ -88,12 +89,14 @@ public PlatformTransactionManager transactionManager(DataSource dataSource) {
 
     @Bean
     public MyBatisPagingItemReader<PersonRecord> reader(SqlSessionFactory sqlSessionFactory) {
-        SelectStatementProvider selectStatement =  SpringBatchUtility.selectForPaging(person.allColumns())
+        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();
+                .render(SpringBatchUtility.SPRING_BATCH_PAGING_ITEM_READER_RENDERING_STRATEGY);
 
         MyBatisPagingItemReader<PersonRecord> reader = new MyBatisPagingItemReader<>();
         reader.setQueryId(PersonMapper.class.getName() + ".selectMany");
diff --git a/src/test/java/examples/springbatch/paging/SpringBatchPagingTest.java b/src/test/java/examples/springbatch/paging/SpringBatchPagingTest.java
index 879270488..c153c8078 100644
--- a/src/test/java/examples/springbatch/paging/SpringBatchPagingTest.java
+++ b/src/test/java/examples/springbatch/paging/SpringBatchPagingTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/type_conversion/MyFilesDynamicSqlSupport.java b/src/test/java/examples/type_conversion/MyFilesDynamicSqlSupport.java
index 48d951b5d..7a58cab44 100644
--- a/src/test/java/examples/type_conversion/MyFilesDynamicSqlSupport.java
+++ b/src/test/java/examples/type_conversion/MyFilesDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/type_conversion/MyFilesMapper.java b/src/test/java/examples/type_conversion/MyFilesMapper.java
index 87b5718b6..1d50774f3 100644
--- a/src/test/java/examples/type_conversion/MyFilesMapper.java
+++ b/src/test/java/examples/type_conversion/MyFilesMapper.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/type_conversion/ToBase64.java b/src/test/java/examples/type_conversion/ToBase64.java
index 97d8ad097..46cdb26ed 100644
--- a/src/test/java/examples/type_conversion/ToBase64.java
+++ b/src/test/java/examples/type_conversion/ToBase64.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -18,6 +18,7 @@
 import java.sql.JDBCType;
 import java.util.Optional;
 
+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;
@@ -25,7 +26,7 @@
 
 public class ToBase64 extends AbstractTypeConvertingFunction<byte[], String, ToBase64> {
 
-    protected ToBase64(BindableColumn<byte[]> column) {
+    protected ToBase64(BasicColumn column) {
         super(column);
     }
 
@@ -36,11 +37,8 @@ public Optional<JDBCType> jdbcType() {
 
     @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();
+        return column.render(renderingContext)
+                .mapFragment(f -> "TO_BASE64(" + f + ")"); //$NON-NLS-1$ //$NON-NLS-2$
     }
 
     @Override
diff --git a/src/test/java/examples/type_conversion/TypeConversionTest.java b/src/test/java/examples/type_conversion/TypeConversionTest.java
index 7aa0e0f9f..90846166e 100644
--- a/src/test/java/examples/type_conversion/TypeConversionTest.java
+++ b/src/test/java/examples/type_conversion/TypeConversionTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/examples/type_conversion/package-info.java b/src/test/java/examples/type_conversion/package-info.java
new file mode 100644
index 000000000..4848de776
--- /dev/null
+++ b/src/test/java/examples/type_conversion/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 examples.type_conversion;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/src/test/java/issues/gh100/FromGroupByTest.java b/src/test/java/issues/gh100/FromGroupByTest.java
index 65b095f68..9c2ee51fb 100644
--- a/src/test/java/issues/gh100/FromGroupByTest.java
+++ b/src/test/java/issues/gh100/FromGroupByTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -105,7 +105,7 @@ void testFromGroupByLimitB3() {
 
         QueryExpressionDSL<SelectModel>.GroupByFinisher builder2 = builder1.groupBy(StudentDynamicSqlSupport.name);
 
-        SelectDSL<SelectModel>.LimitFinisher builder3 = builder2.limit(3);
+        var builder3 = builder2.limit(3);
 
         String expected = "select name, count(*)"
                 + " from student"
@@ -162,7 +162,7 @@ void testFromGroupByOffsetB3() {
 
         QueryExpressionDSL<SelectModel>.GroupByFinisher builder2 = builder1.groupBy(StudentDynamicSqlSupport.name);
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder3 = builder2.offset(3);
+        var builder3 = builder2.offset(3);
 
         String expected = "select name, count(*)"
                 + " from student"
@@ -219,7 +219,7 @@ void testFromGroupByFetchFirstB3() {
 
         QueryExpressionDSL<SelectModel>.GroupByFinisher builder2 = builder1.groupBy(StudentDynamicSqlSupport.name);
 
-        SelectDSL<SelectModel>.RowsOnlyFinisher builder3 = builder2.fetchFirst(2).rowsOnly();
+        var builder3 = builder2.fetchFirst(2).rowsOnly();
 
         String expected = "select name, count(*)"
                 + " from student"
@@ -363,7 +363,7 @@ void testFromGroupByOrderByOffsetB4() {
 
         SelectDSL<SelectModel> builder3 = builder2.orderBy(StudentDynamicSqlSupport.name);
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder4 = builder3.offset(2);
+        var builder4 = builder3.offset(2);
 
         String expected = "select name, count(*)"
                 + " from student"
diff --git a/src/test/java/issues/gh100/FromJoinWhereTest.java b/src/test/java/issues/gh100/FromJoinWhereTest.java
index 651580e95..e36706303 100644
--- a/src/test/java/issues/gh100/FromJoinWhereTest.java
+++ b/src/test/java/issues/gh100/FromJoinWhereTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -32,7 +32,7 @@ void testNormalUsage() {
         SelectStatementProvider selectStatement = select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
                 .from(StudentDynamicSqlSupport.student)
                 .join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid))
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid))
                 .where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"))
                 .union()
                 .select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
@@ -76,7 +76,7 @@ void testFromJoinB1() {
                 .from(StudentDynamicSqlSupport.student);
 
         builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         String expected = "select student.id, student.name, student.idcard"
                 + " from student"
@@ -93,7 +93,7 @@ void testFromJoinB2() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         String expected = "select student.id, student.name, student.idcard"
                 + " from student"
@@ -110,7 +110,7 @@ void testfromJoinWhereB1() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -130,7 +130,7 @@ void testfromJoinWhereB2() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -150,7 +150,7 @@ void testfromJoinWhereB3() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -170,7 +170,7 @@ void testFromJoinWhereUnionB1() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -197,7 +197,7 @@ void testFromJoinWhereUnionB2() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -224,7 +224,7 @@ void testFromJoinWhereUnionB3() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -251,7 +251,7 @@ void testFromJoinWhereUnionB4() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -278,7 +278,7 @@ void testFromJoinWhereUnionUnionB1() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -316,7 +316,7 @@ void testFromJoinWhereUnionUnionB2() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -354,7 +354,7 @@ void testFromJoinWhereUnionUnionB3() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -392,7 +392,7 @@ void testFromJoinWhereUnionUnionB4() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -430,7 +430,7 @@ void testFromJoinWhereUnionUnionB5() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -468,7 +468,7 @@ void testFromJoinWhereUnionOrderByB1() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -498,7 +498,7 @@ void testFromJoinWhereUnionOrderByB2() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -528,7 +528,7 @@ void testFromJoinWhereUnionOrderByB3() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -558,7 +558,7 @@ void testFromJoinWhereUnionOrderByB4() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -588,7 +588,7 @@ void testFromJoinWhereUnionOrderByB5() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -618,7 +618,7 @@ void testFromJoinWhereUnionOrderByLimitB1() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -651,7 +651,7 @@ void testFromJoinWhereUnionOrderByLimitB2() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -684,7 +684,7 @@ void testFromJoinWhereUnionOrderByLimitB3() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -717,7 +717,7 @@ void testFromJoinWhereUnionOrderByLimitB4() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -750,7 +750,7 @@ void testFromJoinWhereUnionOrderByLimitB5() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -783,7 +783,7 @@ void testFromJoinWhereUnionOrderByLimitB6() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -793,7 +793,7 @@ void testFromJoinWhereUnionOrderByLimitB6() {
 
         SelectDSL<SelectModel> builder5 = builder4.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.LimitFinisher builder6 = builder5.limit(3);
+        var builder6 = builder5.limit(3);
 
         String expected = "select student.id, student.name, student.idcard"
                 + " from student"
@@ -816,7 +816,7 @@ void testFromJoinWhereUnionOrderByLimitOffsetB1() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -826,7 +826,7 @@ void testFromJoinWhereUnionOrderByLimitOffsetB1() {
 
         SelectDSL<SelectModel> builder5 = builder4.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.LimitFinisher builder6 = builder5.limit(3);
+        var builder6 = builder5.limit(3);
 
         builder6.offset(2);
 
@@ -852,7 +852,7 @@ void testFromJoinWhereUnionOrderByLimitOffsetB2() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -862,7 +862,7 @@ void testFromJoinWhereUnionOrderByLimitOffsetB2() {
 
         SelectDSL<SelectModel> builder5 = builder4.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.LimitFinisher builder6 = builder5.limit(3);
+        var builder6 = builder5.limit(3);
 
         builder6.offset(2);
 
@@ -888,7 +888,7 @@ void testFromJoinWhereUnionOrderByLimitOffsetB3() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -898,7 +898,7 @@ void testFromJoinWhereUnionOrderByLimitOffsetB3() {
 
         SelectDSL<SelectModel> builder5 = builder4.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.LimitFinisher builder6 = builder5.limit(3);
+        var builder6 = builder5.limit(3);
 
         builder6.offset(2);
 
@@ -924,7 +924,7 @@ void testFromJoinWhereUnionOrderByLimitOffsetB4() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -934,7 +934,7 @@ void testFromJoinWhereUnionOrderByLimitOffsetB4() {
 
         SelectDSL<SelectModel> builder5 = builder4.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.LimitFinisher builder6 = builder5.limit(3);
+        var builder6 = builder5.limit(3);
 
         builder6.offset(2);
 
@@ -960,7 +960,7 @@ void testFromJoinWhereUnionOrderByLimitOffsetB5() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -970,7 +970,7 @@ void testFromJoinWhereUnionOrderByLimitOffsetB5() {
 
         SelectDSL<SelectModel> builder5 = builder4.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.LimitFinisher builder6 = builder5.limit(3);
+        var builder6 = builder5.limit(3);
 
         builder6.offset(2);
 
@@ -996,7 +996,7 @@ void testFromJoinWhereUnionOrderByLimitOffsetB6() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1006,7 +1006,7 @@ void testFromJoinWhereUnionOrderByLimitOffsetB6() {
 
         SelectDSL<SelectModel> builder5 = builder4.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.LimitFinisher builder6 = builder5.limit(3);
+        var builder6 = builder5.limit(3);
 
         builder6.offset(2);
 
@@ -1032,7 +1032,7 @@ void testFromJoinWhereUnionOrderByLimitOffsetB7() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1042,9 +1042,9 @@ void testFromJoinWhereUnionOrderByLimitOffsetB7() {
 
         SelectDSL<SelectModel> builder5 = builder4.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.LimitFinisher builder6 = builder5.limit(3);
+        var builder6 = builder5.limit(3);
 
-        SelectDSL<SelectModel>.OffsetFinisher builder7 = builder6.offset(2);
+        var builder7 = builder6.offset(2);
 
         String expected = "select student.id, student.name, student.idcard"
                 + " from student"
@@ -1068,7 +1068,7 @@ void testFromJoinWhereUnionOrderByOffsetB1() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1102,7 +1102,7 @@ void testFromJoinWhereUnionOrderByOffsetB2() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1136,7 +1136,7 @@ void testFromJoinWhereUnionOrderByOffsetB3() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1170,7 +1170,7 @@ void testFromJoinWhereUnionOrderByOffsetB4() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1204,7 +1204,7 @@ void testFromJoinWhereUnionOrderByOffsetB5() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1238,7 +1238,7 @@ void testFromJoinWhereUnionOrderByOffsetB6() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1248,7 +1248,7 @@ void testFromJoinWhereUnionOrderByOffsetB6() {
 
         SelectDSL<SelectModel> builder5 = builder4.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder6 = builder5.offset(2);
+        var builder6 = builder5.offset(2);
 
         String expected = "select student.id, student.name, student.idcard"
                 + " from student"
@@ -1272,7 +1272,7 @@ void testFromJoinWhereUnionOrderByOffsetFetchFirstB1() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1282,7 +1282,7 @@ void testFromJoinWhereUnionOrderByOffsetFetchFirstB1() {
 
         SelectDSL<SelectModel> builder5 = builder4.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder6 = builder5.offset(2);
+        var builder6 = builder5.offset(2);
 
         builder6.fetchFirst(3).rowsOnly();
 
@@ -1308,7 +1308,7 @@ void testFromJoinWhereUnionOrderByOffsetFetchFirstB2() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1318,7 +1318,7 @@ void testFromJoinWhereUnionOrderByOffsetFetchFirstB2() {
 
         SelectDSL<SelectModel> builder5 = builder4.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder6 = builder5.offset(2);
+        var builder6 = builder5.offset(2);
 
         builder6.fetchFirst(3).rowsOnly();
 
@@ -1344,7 +1344,7 @@ void testFromJoinWhereUnionOrderByOffsetFetchFirstB3() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1354,7 +1354,7 @@ void testFromJoinWhereUnionOrderByOffsetFetchFirstB3() {
 
         SelectDSL<SelectModel> builder5 = builder4.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder6 = builder5.offset(2);
+        var builder6 = builder5.offset(2);
 
         builder6.fetchFirst(3).rowsOnly();
 
@@ -1380,7 +1380,7 @@ void testFromJoinWhereUnionOrderByOffsetFetchFirstB4() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1390,7 +1390,7 @@ void testFromJoinWhereUnionOrderByOffsetFetchFirstB4() {
 
         SelectDSL<SelectModel> builder5 = builder4.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder6 = builder5.offset(2);
+        var builder6 = builder5.offset(2);
 
         builder6.fetchFirst(3).rowsOnly();
 
@@ -1416,7 +1416,7 @@ void testFromJoinWhereUnionOrderByOffsetFetchFirstB5() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1426,7 +1426,7 @@ void testFromJoinWhereUnionOrderByOffsetFetchFirstB5() {
 
         SelectDSL<SelectModel> builder5 = builder4.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder6 = builder5.offset(2);
+        var builder6 = builder5.offset(2);
 
         builder6.fetchFirst(3).rowsOnly();
 
@@ -1452,7 +1452,7 @@ void testFromJoinWhereUnionOrderByOffsetFetchFirstB6() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1462,7 +1462,7 @@ void testFromJoinWhereUnionOrderByOffsetFetchFirstB6() {
 
         SelectDSL<SelectModel> builder5 = builder4.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder6 = builder5.offset(2);
+        var builder6 = builder5.offset(2);
 
         builder6.fetchFirst(3).rowsOnly();
 
@@ -1488,7 +1488,7 @@ void testFromJoinWhereUnionOrderByOffsetFetchFirstB7() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1498,9 +1498,9 @@ void testFromJoinWhereUnionOrderByOffsetFetchFirstB7() {
 
         SelectDSL<SelectModel> builder5 = builder4.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder6 = builder5.offset(2);
+        var builder6 = builder5.offset(2);
 
-        SelectDSL<SelectModel>.RowsOnlyFinisher builder7 = builder6.fetchFirst(3).rowsOnly();
+        var builder7 = builder6.fetchFirst(3).rowsOnly();
 
         String expected = "select student.id, student.name, student.idcard"
                 + " from student"
@@ -1524,7 +1524,7 @@ void testFromJoinWhereUnionOrderByFetchFirstB1() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1557,7 +1557,7 @@ void testFromJoinWhereUnionOrderByFetchFirstB2() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1590,7 +1590,7 @@ void testFromJoinWhereUnionOrderByFetchFirstB3() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1623,7 +1623,7 @@ void testFromJoinWhereUnionOrderByFetchFirstB4() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1656,7 +1656,7 @@ void testFromJoinWhereUnionOrderByFetchFirstB5() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1689,7 +1689,7 @@ void testFromJoinWhereUnionOrderByFetchFirstB6() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1699,7 +1699,7 @@ void testFromJoinWhereUnionOrderByFetchFirstB6() {
 
         SelectDSL<SelectModel> builder5 = builder4.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.RowsOnlyFinisher builder6 = builder5.fetchFirst(3).rowsOnly();
+        var builder6 = builder5.fetchFirst(3).rowsOnly();
 
         String expected = "select student.id, student.name, student.idcard"
                 + " from student"
@@ -1722,7 +1722,7 @@ void testFromJoinWhereUnionLimitB1() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1752,7 +1752,7 @@ void testFromJoinWhereUnionLimitB2() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1782,7 +1782,7 @@ void testFromJoinWhereUnionLimitB3() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1812,7 +1812,7 @@ void testFromJoinWhereUnionLimitB4() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1842,7 +1842,7 @@ void testFromJoinWhereUnionLimitB5() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1850,7 +1850,7 @@ void testFromJoinWhereUnionLimitB5() {
                 .select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
                 .from(StudentDynamicSqlSupport.student);
 
-        SelectDSL<SelectModel>.LimitFinisher builder5 = builder4.limit(3);
+        var builder5 = builder4.limit(3);
 
         String expected = "select student.id, student.name, student.idcard"
                 + " from student"
@@ -1872,7 +1872,7 @@ void testFromJoinWhereUnionLimitOffsetB1() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1880,7 +1880,7 @@ void testFromJoinWhereUnionLimitOffsetB1() {
                 .select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
                 .from(StudentDynamicSqlSupport.student);
 
-        SelectDSL<SelectModel>.LimitFinisher builder5 = builder4.limit(3);
+        var builder5 = builder4.limit(3);
 
         builder5.offset(2);
 
@@ -1905,7 +1905,7 @@ void testFromJoinWhereUnionLimitOffsetB2() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1913,7 +1913,7 @@ void testFromJoinWhereUnionLimitOffsetB2() {
                 .select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
                 .from(StudentDynamicSqlSupport.student);
 
-        SelectDSL<SelectModel>.LimitFinisher builder5 = builder4.limit(3);
+        var builder5 = builder4.limit(3);
 
         builder5.offset(2);
 
@@ -1938,7 +1938,7 @@ void testFromJoinWhereUnionLimitOffsetB3() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1946,7 +1946,7 @@ void testFromJoinWhereUnionLimitOffsetB3() {
                 .select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
                 .from(StudentDynamicSqlSupport.student);
 
-        SelectDSL<SelectModel>.LimitFinisher builder5 = builder4.limit(3);
+        var builder5 = builder4.limit(3);
 
         builder5.offset(2);
 
@@ -1971,7 +1971,7 @@ void testFromJoinWhereUnionLimitOffsetB4() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -1979,7 +1979,7 @@ void testFromJoinWhereUnionLimitOffsetB4() {
                 .select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
                 .from(StudentDynamicSqlSupport.student);
 
-        SelectDSL<SelectModel>.LimitFinisher builder5 = builder4.limit(3);
+        var builder5 = builder4.limit(3);
 
         builder5.offset(2);
 
@@ -2004,7 +2004,7 @@ void testFromJoinWhereUnionLimitOffsetB5() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2012,7 +2012,7 @@ void testFromJoinWhereUnionLimitOffsetB5() {
                 .select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
                 .from(StudentDynamicSqlSupport.student);
 
-        SelectDSL<SelectModel>.LimitFinisher builder5 = builder4.limit(3);
+        var builder5 = builder4.limit(3);
 
         builder5.offset(2);
 
@@ -2037,7 +2037,7 @@ void testFromJoinWhereUnionLimitOffsetB6() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2045,9 +2045,9 @@ void testFromJoinWhereUnionLimitOffsetB6() {
                 .select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
                 .from(StudentDynamicSqlSupport.student);
 
-        SelectDSL<SelectModel>.LimitFinisher builder5 = builder4.limit(3);
+        var builder5 = builder4.limit(3);
 
-        SelectDSL<SelectModel>.OffsetFinisher builder6 = builder5.offset(2);
+        var builder6 = builder5.offset(2);
 
         String expected = "select student.id, student.name, student.idcard"
                 + " from student"
@@ -2070,7 +2070,7 @@ void testFromJoinWhereUnionOffsetB1() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2100,7 +2100,7 @@ void testFromJoinWhereUnionOffsetB2() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2130,7 +2130,7 @@ void testFromJoinWhereUnionOffsetB3() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2160,7 +2160,7 @@ void testFromJoinWhereUnionOffsetB4() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2190,7 +2190,7 @@ void testFromJoinWhereUnionOffsetB5() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2198,7 +2198,7 @@ void testFromJoinWhereUnionOffsetB5() {
                 .select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
                 .from(StudentDynamicSqlSupport.student);
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder5 = builder4.offset(2);
+        var builder5 = builder4.offset(2);
 
         String expected = "select student.id, student.name, student.idcard"
                 + " from student"
@@ -2220,7 +2220,7 @@ void testFromJoinWhereUnionOffsetFetchFirstB1() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2228,7 +2228,7 @@ void testFromJoinWhereUnionOffsetFetchFirstB1() {
                 .select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
                 .from(StudentDynamicSqlSupport.student);
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder5 = builder4.offset(2);
+        var builder5 = builder4.offset(2);
 
         builder5.fetchFirst(2).rowsOnly();
 
@@ -2253,7 +2253,7 @@ void testFromJoinWhereUnionOffsetFetchFirstB2() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2261,7 +2261,7 @@ void testFromJoinWhereUnionOffsetFetchFirstB2() {
                 .select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
                 .from(StudentDynamicSqlSupport.student);
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder5 = builder4.offset(2);
+        var builder5 = builder4.offset(2);
 
         builder5.fetchFirst(2).rowsOnly();
 
@@ -2286,7 +2286,7 @@ void testFromJoinWhereUnionOffsetFetchFirstB3() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2294,7 +2294,7 @@ void testFromJoinWhereUnionOffsetFetchFirstB3() {
                 .select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
                 .from(StudentDynamicSqlSupport.student);
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder5 = builder4.offset(2);
+        var builder5 = builder4.offset(2);
 
         builder5.fetchFirst(2).rowsOnly();
 
@@ -2319,7 +2319,7 @@ void testFromJoinWhereUnionOffsetFetchFirstB4() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2327,7 +2327,7 @@ void testFromJoinWhereUnionOffsetFetchFirstB4() {
                 .select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
                 .from(StudentDynamicSqlSupport.student);
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder5 = builder4.offset(2);
+        var builder5 = builder4.offset(2);
 
         builder5.fetchFirst(2).rowsOnly();
 
@@ -2352,7 +2352,7 @@ void testFromJoinWhereUnionOffsetFetchFirstB5() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2360,7 +2360,7 @@ void testFromJoinWhereUnionOffsetFetchFirstB5() {
                 .select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
                 .from(StudentDynamicSqlSupport.student);
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder5 = builder4.offset(2);
+        var builder5 = builder4.offset(2);
 
         builder5.fetchFirst(2).rowsOnly();
 
@@ -2385,7 +2385,7 @@ void testFromJoinWhereUnionOffsetFetchFirstB6() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2393,9 +2393,9 @@ void testFromJoinWhereUnionOffsetFetchFirstB6() {
                 .select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
                 .from(StudentDynamicSqlSupport.student);
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder5 = builder4.offset(2);
+        var builder5 = builder4.offset(2);
 
-        SelectDSL<SelectModel>.RowsOnlyFinisher builder6 = builder5.fetchFirst(2).rowsOnly();
+        var builder6 = builder5.fetchFirst(2).rowsOnly();
 
         String expected = "select student.id, student.name, student.idcard"
                 + " from student"
@@ -2418,7 +2418,7 @@ void testFromJoinWhereUnionFetchFirstB1() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2448,7 +2448,7 @@ void testFromJoinWhereUnionFetchFirstB2() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2478,7 +2478,7 @@ void testFromJoinWhereUnionFetchFirstB3() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2508,7 +2508,7 @@ void testFromJoinWhereUnionFetchFirstB4() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2538,7 +2538,7 @@ void testFromJoinWhereUnionFetchFirstB5() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2546,7 +2546,7 @@ void testFromJoinWhereUnionFetchFirstB5() {
                 .select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
                 .from(StudentDynamicSqlSupport.student);
 
-        SelectDSL<SelectModel>.RowsOnlyFinisher builder5 = builder4.fetchFirst(2).rowsOnly();
+        var builder5 = builder4.fetchFirst(2).rowsOnly();
 
         String expected = "select student.id, student.name, student.idcard"
                 + " from student"
@@ -2568,7 +2568,7 @@ void testFromJoinWhereOrderByB1() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2591,7 +2591,7 @@ void testFromJoinWhereOrderByB2() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2614,7 +2614,7 @@ void testFromJoinWhereOrderByB3() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2637,7 +2637,7 @@ void testFromJoinWhereOrderByB4() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2660,7 +2660,7 @@ void testFromJoinWhereOrderByLimitB1() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2686,7 +2686,7 @@ void testFromJoinWhereOrderByLimitB2() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2712,7 +2712,7 @@ void testFromJoinWhereOrderByLimitB3() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2738,7 +2738,7 @@ void testFromJoinWhereOrderByLimitB4() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2764,13 +2764,13 @@ void testFromJoinWhereOrderByLimitB5() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
         SelectDSL<SelectModel> builder4 = builder3.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.LimitFinisher builder5 = builder4.limit(3);
+        var builder5 = builder4.limit(3);
 
         String expected = "select student.id, student.name, student.idcard"
                 + " from student"
@@ -2790,13 +2790,13 @@ void testFromJoinWhereOrderByLimitOffsetB1() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
         SelectDSL<SelectModel> builder4 = builder3.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.LimitFinisher builder5 = builder4.limit(3);
+        var builder5 = builder4.limit(3);
 
         builder5.offset(2);
 
@@ -2819,13 +2819,13 @@ void testFromJoinWhereOrderByLimitOffsetB2() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
         SelectDSL<SelectModel> builder4 = builder3.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.LimitFinisher builder5 = builder4.limit(3);
+        var builder5 = builder4.limit(3);
 
         builder5.offset(2);
 
@@ -2848,13 +2848,13 @@ void testFromJoinWhereOrderByLimitOffsetB3() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
         SelectDSL<SelectModel> builder4 = builder3.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.LimitFinisher builder5 = builder4.limit(3);
+        var builder5 = builder4.limit(3);
 
         builder5.offset(2);
 
@@ -2877,13 +2877,13 @@ void testFromJoinWhereOrderByLimitOffsetB4() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
         SelectDSL<SelectModel> builder4 = builder3.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.LimitFinisher builder5 = builder4.limit(3);
+        var builder5 = builder4.limit(3);
 
         builder5.offset(2);
 
@@ -2906,13 +2906,13 @@ void testFromJoinWhereOrderByLimitOffsetB5() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
         SelectDSL<SelectModel> builder4 = builder3.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.LimitFinisher builder5 = builder4.limit(3);
+        var builder5 = builder4.limit(3);
 
         builder5.offset(2);
 
@@ -2935,15 +2935,15 @@ void testFromJoinWhereOrderByLimitOffsetB6() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
         SelectDSL<SelectModel> builder4 = builder3.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.LimitFinisher builder5 = builder4.limit(3);
+        var builder5 = builder4.limit(3);
 
-        SelectDSL<SelectModel>.OffsetFinisher builder6 = builder5.offset(2);
+        var builder6 = builder5.offset(2);
 
         String expected = "select student.id, student.name, student.idcard"
                 + " from student"
@@ -2964,7 +2964,7 @@ void testFromJoinWhereOrderByOffsetB1() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -2990,7 +2990,7 @@ void testFromJoinWhereOrderByOffsetB2() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -3016,7 +3016,7 @@ void testFromJoinWhereOrderByOffsetB3() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -3042,7 +3042,7 @@ void testFromJoinWhereOrderByOffsetB4() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -3068,13 +3068,13 @@ void testFromJoinWhereOrderByOffsetB5() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
         SelectDSL<SelectModel> builder4 = builder3.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder5 = builder4.offset(2);
+        var builder5 = builder4.offset(2);
 
         String expected = "select student.id, student.name, student.idcard"
                 + " from student"
@@ -3094,13 +3094,13 @@ void testFromJoinWhereOrderByOffsetFetchFirstB1() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
         SelectDSL<SelectModel> builder4 = builder3.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder5 = builder4.offset(2);
+        var builder5 = builder4.offset(2);
 
         builder5.fetchFirst(3).rowsOnly();
 
@@ -3123,13 +3123,13 @@ void testFromJoinWhereOrderByOffsetFetchFirstB2() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
         SelectDSL<SelectModel> builder4 = builder3.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder5 = builder4.offset(2);
+        var builder5 = builder4.offset(2);
 
         builder5.fetchFirst(3).rowsOnly();
 
@@ -3152,13 +3152,13 @@ void testFromJoinWhereOrderByOffsetFetchFirstB3() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
         SelectDSL<SelectModel> builder4 = builder3.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder5 = builder4.offset(2);
+        var builder5 = builder4.offset(2);
 
         builder5.fetchFirst(3).rowsOnly();
 
@@ -3181,13 +3181,13 @@ void testFromJoinWhereOrderByOffsetFetchFirstB4() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
         SelectDSL<SelectModel> builder4 = builder3.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder5 = builder4.offset(2);
+        var builder5 = builder4.offset(2);
 
         builder5.fetchFirst(3).rowsOnly();
 
@@ -3210,13 +3210,13 @@ void testFromJoinWhereOrderByOffsetFetchFirstB5() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
         SelectDSL<SelectModel> builder4 = builder3.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder5 = builder4.offset(2);
+        var builder5 = builder4.offset(2);
 
         builder5.fetchFirst(3).rowsOnly();
 
@@ -3239,15 +3239,15 @@ void testFromJoinWhereOrderByOffsetFetchFirstB6() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
         SelectDSL<SelectModel> builder4 = builder3.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder5 = builder4.offset(2);
+        var builder5 = builder4.offset(2);
 
-        SelectDSL<SelectModel>.RowsOnlyFinisher builder6 = builder5.fetchFirst(3).rowsOnly();
+        var builder6 = builder5.fetchFirst(3).rowsOnly();
 
         String expected = "select student.id, student.name, student.idcard"
                 + " from student"
@@ -3268,7 +3268,7 @@ void testFromJoinWhereOrderByFetchFirstB1() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -3294,7 +3294,7 @@ void testFromJoinWhereOrderByFetchFirstB2() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -3320,7 +3320,7 @@ void testFromJoinWhereOrderByFetchFirstB3() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -3346,7 +3346,7 @@ void testFromJoinWhereOrderByFetchFirstB4() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -3372,13 +3372,13 @@ void testFromJoinWhereOrderByFetchFirstB5() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
         SelectDSL<SelectModel> builder4 = builder3.orderBy(StudentDynamicSqlSupport.id);
 
-        SelectDSL<SelectModel>.RowsOnlyFinisher builder5 = builder4.fetchFirst(3).rowsOnly();
+        var builder5 = builder4.fetchFirst(3).rowsOnly();
 
         String expected = "select student.id, student.name, student.idcard"
                 + " from student"
@@ -3398,7 +3398,7 @@ void testFromJoinWhereLimitB1() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -3421,7 +3421,7 @@ void testFromJoinWhereLimitB2() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -3444,7 +3444,7 @@ void testFromJoinWhereLimitB3() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -3467,11 +3467,11 @@ void testFromJoinWhereLimitB4() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
-        SelectDSL<SelectModel>.LimitFinisher builder4 = builder3.limit(2);
+        var builder4 = builder3.limit(2);
 
         String expected = "select student.id, student.name, student.idcard"
                 + " from student"
@@ -3490,11 +3490,11 @@ void testFromJoinWhereLimitOffsetB1() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
-        SelectDSL<SelectModel>.LimitFinisher builder4 = builder3.limit(2);
+        var builder4 = builder3.limit(2);
 
         builder4.offset(3);
 
@@ -3516,11 +3516,11 @@ void testFromJoinWhereLimitOffsetB2() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
-        SelectDSL<SelectModel>.LimitFinisher builder4 = builder3.limit(2);
+        var builder4 = builder3.limit(2);
 
         builder4.offset(3);
 
@@ -3542,11 +3542,11 @@ void testFromJoinWhereLimitOffsetB3() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
-        SelectDSL<SelectModel>.LimitFinisher builder4 = builder3.limit(2);
+        var builder4 = builder3.limit(2);
 
         builder4.offset(3);
 
@@ -3568,11 +3568,11 @@ void testFromJoinWhereLimitOffsetB4() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
-        SelectDSL<SelectModel>.LimitFinisher builder4 = builder3.limit(2);
+        var builder4 = builder3.limit(2);
 
         builder4.offset(3);
 
@@ -3594,13 +3594,13 @@ void testFromJoinWhereLimitOffsetB5() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
-        SelectDSL<SelectModel>.LimitFinisher builder4 = builder3.limit(2);
+        var builder4 = builder3.limit(2);
 
-        SelectDSL<SelectModel>.OffsetFinisher builder5 = builder4.offset(3);
+        var builder5 = builder4.offset(3);
 
         String expected = "select student.id, student.name, student.idcard"
                 + " from student"
@@ -3620,7 +3620,7 @@ void testFromJoinWhereOffsetB1() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -3643,7 +3643,7 @@ void testFromJoinWhereOffsetB2() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -3666,7 +3666,7 @@ void testFromJoinWhereOffsetB3() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -3689,11 +3689,11 @@ void testFromJoinWhereOffsetB4() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder4 = builder3.offset(3);
+        var builder4 = builder3.offset(3);
 
         String expected = "select student.id, student.name, student.idcard"
                 + " from student"
@@ -3712,11 +3712,11 @@ void testFromJoinWhereOffsetFetchFirstB1() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder4 = builder3.offset(3);
+        var builder4 = builder3.offset(3);
 
         builder4.fetchFirst(2).rowsOnly();
 
@@ -3738,11 +3738,11 @@ void testFromJoinWhereOffsetFetchFirstB2() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder4 = builder3.offset(3);
+        var builder4 = builder3.offset(3);
 
         builder4.fetchFirst(2).rowsOnly();
 
@@ -3764,11 +3764,11 @@ void testFromJoinWhereOffsetFetchFirstB3() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder4 = builder3.offset(3);
+        var builder4 = builder3.offset(3);
 
         builder4.fetchFirst(2).rowsOnly();
 
@@ -3790,11 +3790,11 @@ void testFromJoinWhereOffsetFetchFirstB4() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder4 = builder3.offset(3);
+        var builder4 = builder3.offset(3);
 
         builder4.fetchFirst(2).rowsOnly();
 
@@ -3816,13 +3816,13 @@ void testFromJoinWhereOffsetFetchFirstB5() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
-        SelectDSL<SelectModel>.OffsetFirstFinisher builder4 = builder3.offset(3);
+        var builder4 = builder3.offset(3);
 
-        SelectDSL<SelectModel>.RowsOnlyFinisher builder5 = builder4.fetchFirst(2).rowsOnly();
+        var builder5 = builder4.fetchFirst(2).rowsOnly();
 
         String expected = "select student.id, student.name, student.idcard"
                 + " from student"
@@ -3842,7 +3842,7 @@ void testFromJoinWhereFetchFirstB1() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -3865,7 +3865,7 @@ void testFromJoinWhereFetchFirstB2() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -3888,7 +3888,7 @@ void testFromJoinWhereFetchFirstB3() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -3911,11 +3911,11 @@ void testFromJoinWhereFetchFirstB4() {
                 .from(StudentDynamicSqlSupport.student);
 
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder2 = builder1.join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         QueryExpressionDSL<SelectModel>.QueryExpressionWhereBuilder builder3 = builder2.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
-        SelectDSL<SelectModel>.RowsOnlyFinisher builder4 = builder3.fetchFirst(2).rowsOnly();
+        var builder4 = builder3.fetchFirst(2).rowsOnly();
 
         String expected = "select student.id, student.name, student.idcard"
                 + " from student"
diff --git a/src/test/java/issues/gh100/Issue100StartAfterJoinTest.java b/src/test/java/issues/gh100/Issue100StartAfterJoinTest.java
index 9c3631c4d..6be283173 100644
--- a/src/test/java/issues/gh100/Issue100StartAfterJoinTest.java
+++ b/src/test/java/issues/gh100/Issue100StartAfterJoinTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -32,7 +32,7 @@ void testSuccessiveBuild02() {
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder = select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
                 .from(StudentDynamicSqlSupport.student)
                 .join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         builder.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
@@ -50,7 +50,7 @@ void testSuccessiveBuild03() {
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder = select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
                 .from(StudentDynamicSqlSupport.student)
                 .join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         SelectDSL<SelectModel> selectModel = builder.orderBy(StudentDynamicSqlSupport.id);
 
@@ -71,7 +71,7 @@ void testSuccessiveBuild04() {
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder = select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
                 .from(StudentDynamicSqlSupport.student)
                 .join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         builder.limit(3);
 
@@ -89,7 +89,7 @@ void testSuccessiveBuild05() {
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder = select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
                 .from(StudentDynamicSqlSupport.student)
                 .join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         builder.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"))
         .orderBy(StudentDynamicSqlSupport.id)
@@ -113,7 +113,7 @@ void testSuccessiveBuild06() {
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder = select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
                 .from(StudentDynamicSqlSupport.student)
                 .join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         builder.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"))
         .orderBy(StudentDynamicSqlSupport.id)
@@ -135,7 +135,7 @@ void testSuccessiveBuild07() {
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder = select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
                 .from(StudentDynamicSqlSupport.student)
                 .join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         builder.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"))
         .orderBy(StudentDynamicSqlSupport.id)
@@ -159,7 +159,7 @@ void testSuccessiveBuild08() {
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher builder = select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
                 .from(StudentDynamicSqlSupport.student)
                 .join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         builder.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"))
         .orderBy(StudentDynamicSqlSupport.id)
diff --git a/src/test/java/issues/gh100/Issue100Test.java b/src/test/java/issues/gh100/Issue100Test.java
index f0411c774..3f848fea5 100644
--- a/src/test/java/issues/gh100/Issue100Test.java
+++ b/src/test/java/issues/gh100/Issue100Test.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -31,7 +31,7 @@ void testNormalUsage() {
         SelectStatementProvider selectStatement = select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
                 .from(StudentDynamicSqlSupport.student)
                 .join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid))
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid))
                 .where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"))
                 .union()
                 .select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
@@ -60,7 +60,7 @@ void testSuccessiveBuild01() {
                 .from(StudentDynamicSqlSupport.student);
 
         builder.join(StudentRegDynamicSqlSupport.studentReg)
-        .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+        .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         SelectStatementProvider selectStatement = builder.build()
                 .render(RenderingStrategies.MYBATIS3);
@@ -76,7 +76,7 @@ void testSuccessiveBuild02() {
                 .from(StudentDynamicSqlSupport.student);
 
         builder.join(StudentRegDynamicSqlSupport.studentReg)
-        .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid))
+        .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid))
         .where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
         SelectStatementProvider selectStatement = builder.build()
@@ -94,7 +94,7 @@ void testSuccessiveBuild03() {
                 .from(StudentDynamicSqlSupport.student);
 
         builder.join(StudentRegDynamicSqlSupport.studentReg)
-        .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid))
+        .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid))
         .where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"))
         .orderBy(StudentDynamicSqlSupport.id);
 
@@ -114,7 +114,7 @@ void testSuccessiveBuild04() {
                 .from(StudentDynamicSqlSupport.student);
 
         builder.join(StudentRegDynamicSqlSupport.studentReg)
-        .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid))
+        .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid))
         .where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"))
         .orderBy(StudentDynamicSqlSupport.id)
         .limit(3);
@@ -136,7 +136,7 @@ void testSuccessiveBuild05() {
                 .from(StudentDynamicSqlSupport.student);
 
         builder.join(StudentRegDynamicSqlSupport.studentReg)
-        .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid))
+        .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid))
         .where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"))
         .orderBy(StudentDynamicSqlSupport.id)
         .limit(3)
@@ -182,8 +182,8 @@ void testSuccessiveBuild07() {
                 .from(StudentDynamicSqlSupport.student);
 
         builder.join(StudentRegDynamicSqlSupport.studentReg)
-        .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid))
-        .where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"))
+        .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid))
+        .where(StudentDynamicSqlSupport.idcard, equalTo("fred"))
         .orderBy(StudentDynamicSqlSupport.id)
         .offset(2)
         .fetchFirst(3).rowsOnly();
@@ -206,7 +206,7 @@ void testSuccessiveBuild08() {
                 .from(StudentDynamicSqlSupport.student);
 
         builder.join(StudentRegDynamicSqlSupport.studentReg)
-        .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid))
+        .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid))
         .where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"))
         .orderBy(StudentDynamicSqlSupport.id)
         .fetchFirst(3).rowsOnly();
@@ -227,7 +227,7 @@ void test3() {
         QueryExpressionDSL<SelectModel>.JoinSpecificationFinisher on = select(StudentDynamicSqlSupport.id, StudentDynamicSqlSupport.name, StudentDynamicSqlSupport.idcard)
                 .from(StudentDynamicSqlSupport.student)
                 .join(StudentRegDynamicSqlSupport.studentReg)
-                .on(StudentDynamicSqlSupport.id, equalTo(StudentRegDynamicSqlSupport.studentid));
+                .on(StudentDynamicSqlSupport.id, isEqualTo(StudentRegDynamicSqlSupport.studentid));
 
         on.where(StudentDynamicSqlSupport.idcard, isEqualTo("fred"));
 
diff --git a/src/test/java/issues/gh100/StudentDynamicSqlSupport.java b/src/test/java/issues/gh100/StudentDynamicSqlSupport.java
index 982cdba47..e169662f8 100644
--- a/src/test/java/issues/gh100/StudentDynamicSqlSupport.java
+++ b/src/test/java/issues/gh100/StudentDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/issues/gh100/StudentRegDynamicSqlSupport.java b/src/test/java/issues/gh100/StudentRegDynamicSqlSupport.java
index 4c715db21..e1ddc3e84 100644
--- a/src/test/java/issues/gh100/StudentRegDynamicSqlSupport.java
+++ b/src/test/java/issues/gh100/StudentRegDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/issues/gh105/Issue105Test.java b/src/test/java/issues/gh105/Issue105Test.java
index c40127a3d..63d2b2f16 100644
--- a/src/test/java/issues/gh105/Issue105Test.java
+++ b/src/test/java/issues/gh105/Issue105Test.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,12 +19,9 @@
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mybatis.dynamic.sql.SqlBuilder.*;
 
-import java.util.Objects;
-
 import org.junit.jupiter.api.Test;
 import org.mybatis.dynamic.sql.render.RenderingStrategies;
 import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
-import org.mybatis.dynamic.sql.util.Predicates;
 
 class Issue105Test {
 
@@ -35,8 +32,8 @@ void testFuzzyLikeBothPresent() {
 
         SelectStatementProvider selectStatement = select(id, firstName, lastName)
                 .from(person)
-                .where(firstName, isLike(fName).filter(Objects::nonNull).map(s -> "%" + s + "%"))
-                .and(lastName, isLike(lName).filter(Objects::nonNull).map(s -> "%" + s + "%"))
+                .where(firstName, isLike(fName).map(s -> "%" + s + "%"))
+                .and(lastName, isLike(lName).map(s -> "%" + s + "%"))
                 .build()
                 .render(RenderingStrategies.MYBATIS3);
 
@@ -52,13 +49,12 @@ void testFuzzyLikeBothPresent() {
 
     @Test
     void testFuzzyLikeFirstNameNull() {
-        String fName = null;
         String lName = "Flintstone";
 
         SelectStatementProvider selectStatement = select(id, firstName, lastName)
                 .from(person)
-                .where(firstName, isLike(fName).filter(Objects::nonNull).map(SearchUtils::addWildcards))
-                .and(lastName, isLike(lName).filter(Objects::nonNull).map(SearchUtils::addWildcards))
+                .where(firstName, isLikeWhenPresent((String) null).map(SearchUtils::addWildcards))
+                .and(lastName, isLike(lName).map(SearchUtils::addWildcards))
                 .build()
                 .render(RenderingStrategies.MYBATIS3);
 
@@ -73,12 +69,11 @@ void testFuzzyLikeFirstNameNull() {
     @Test
     void testFuzzyLikeLastNameNull() {
         String fName = "Fred";
-        String lName = null;
 
         SelectStatementProvider selectStatement = select(id, firstName, lastName)
                 .from(person)
-                .where(firstName, isLike(fName).filter(Objects::nonNull).map(SearchUtils::addWildcards))
-                .and(lastName, isLike(lName).filter(Objects::nonNull).map(SearchUtils::addWildcards))
+                .where(firstName, isLike(fName).map(SearchUtils::addWildcards))
+                .and(lastName, isLikeWhenPresent((String) null).map(SearchUtils::addWildcards))
                 .build()
                 .render(RenderingStrategies.MYBATIS3);
 
@@ -92,13 +87,10 @@ void testFuzzyLikeLastNameNull() {
 
     @Test
     void testFuzzyLikeBothNull() {
-        String fName = null;
-        String lName = null;
-
         SelectStatementProvider selectStatement = select(id, firstName, lastName)
                 .from(person)
-                .where(firstName, isLike(fName).filter(Objects::nonNull).map(SearchUtils::addWildcards))
-                .and(lastName, isLike(lName).filter(Objects::nonNull).map(SearchUtils::addWildcards))
+                .where(firstName, isLikeWhenPresent((String) null).map(SearchUtils::addWildcards))
+                .and(lastName, isLikeWhenPresent((String) null).map(SearchUtils::addWildcards))
                 .configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true))
                 .build()
                 .render(RenderingStrategies.MYBATIS3);
@@ -522,22 +514,6 @@ void testNotLikeWhenPresentTransform() {
         assertThat(selectStatement.getParameters()).containsEntry("p1", "%fred%");
     }
 
-    @Test
-    void testBetweenTransformWithNull() {
-
-        SelectStatementProvider selectStatement = select(id, firstName, lastName)
-                .from(person)
-                .where(age, isBetween(1).and((Integer) null).filter(Predicates.bothPresent()).map(i1 -> i1 + 1,  i2 -> i2 + 2))
-                .configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true))
-                .build()
-                .render(RenderingStrategies.MYBATIS3);
-
-        String expected = "select person_id, first_name, last_name"
-                + " from Person";
-
-        assertThat(selectStatement.getSelectStatement()).isEqualTo(expected);
-    }
-
     @Test
     void testBetweenWhenPresentTransformWithNull() {
 
@@ -554,22 +530,6 @@ void testBetweenWhenPresentTransformWithNull() {
         assertThat(selectStatement.getSelectStatement()).isEqualTo(expected);
     }
 
-    @Test
-    void testEqualTransformWithNull() {
-
-        SelectStatementProvider selectStatement = select(id, firstName, lastName)
-                .from(person)
-                .where(age, isEqualTo((Integer) null).filter(Objects::nonNull).map(i -> i + 1))
-                .configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true))
-                .build()
-                .render(RenderingStrategies.MYBATIS3);
-
-        String expected = "select person_id, first_name, last_name"
-                + " from Person";
-
-        assertThat(selectStatement.getSelectStatement()).isEqualTo(expected);
-    }
-
     @Test
     void testEqualWhenPresentTransformWithNull() {
 
@@ -586,38 +546,6 @@ void testEqualWhenPresentTransformWithNull() {
         assertThat(selectStatement.getSelectStatement()).isEqualTo(expected);
     }
 
-    @Test
-    void testGreaterThanTransformWithNull() {
-
-        SelectStatementProvider selectStatement = select(id, firstName, lastName)
-                .from(person)
-                .where(age, isGreaterThan((Integer) null).filter(Objects::nonNull).map(i -> i + 1))
-                .configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true))
-                .build()
-                .render(RenderingStrategies.MYBATIS3);
-
-        String expected = "select person_id, first_name, last_name"
-                + " from Person";
-
-        assertThat(selectStatement.getSelectStatement()).isEqualTo(expected);
-    }
-
-    @Test
-    void testGreaterThanOrEqualTransformWithNull() {
-
-        SelectStatementProvider selectStatement = select(id, firstName, lastName)
-                .from(person)
-                .where(age, isGreaterThanOrEqualTo((Integer) null).filter(Objects::nonNull).map(i -> i + 1))
-                .configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true))
-                .build()
-                .render(RenderingStrategies.MYBATIS3);
-
-        String expected = "select person_id, first_name, last_name"
-                + " from Person";
-
-        assertThat(selectStatement.getSelectStatement()).isEqualTo(expected);
-    }
-
     @Test
     void testGreaterThanOrEqualWhenPresentTransformWithNull() {
 
@@ -650,38 +578,6 @@ void testGreaterThanWhenPresentTransformWithNull() {
         assertThat(selectStatement.getSelectStatement()).isEqualTo(expected);
     }
 
-    @Test
-    void testLessThanTransformWithNull() {
-
-        SelectStatementProvider selectStatement = select(id, firstName, lastName)
-                .from(person)
-                .where(age, isLessThan((Integer) null).filter(Objects::nonNull).map(i -> i + 1))
-                .configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true))
-                .build()
-                .render(RenderingStrategies.MYBATIS3);
-
-        String expected = "select person_id, first_name, last_name"
-                + " from Person";
-
-        assertThat(selectStatement.getSelectStatement()).isEqualTo(expected);
-    }
-
-    @Test
-    void testLessThanOrEqualTransformWithNull() {
-
-        SelectStatementProvider selectStatement = select(id, firstName, lastName)
-                .from(person)
-                .where(age, isLessThanOrEqualTo((Integer) null).filter(Objects::nonNull).map(i -> i + 1))
-                .configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true))
-                .build()
-                .render(RenderingStrategies.MYBATIS3);
-
-        String expected = "select person_id, first_name, last_name"
-                + " from Person";
-
-        assertThat(selectStatement.getSelectStatement()).isEqualTo(expected);
-    }
-
     @Test
     void testLessThanOrEqualWhenPresentTransformWithNull() {
 
@@ -714,38 +610,6 @@ void testLessThanWhenPresentTransformWithNull() {
         assertThat(selectStatement.getSelectStatement()).isEqualTo(expected);
     }
 
-    @Test
-    void testLikeTransformWithNull() {
-
-        SelectStatementProvider selectStatement = select(id, firstName, lastName)
-                .from(person)
-                .where(firstName, isLike((String) null).filter(Objects::nonNull).map(SearchUtils::addWildcards))
-                .configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true))
-                .build()
-                .render(RenderingStrategies.MYBATIS3);
-
-        String expected = "select person_id, first_name, last_name"
-                + " from Person";
-
-        assertThat(selectStatement.getSelectStatement()).isEqualTo(expected);
-    }
-
-    @Test
-    void testLikeCaseInsensitiveTransformWithNull() {
-
-        SelectStatementProvider selectStatement = select(id, firstName, lastName)
-                .from(person)
-                .where(firstName, isLikeCaseInsensitive((String) null).filter(Objects::nonNull).map(SearchUtils::addWildcards))
-                .configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true))
-                .build()
-                .render(RenderingStrategies.MYBATIS3);
-
-        String expected = "select person_id, first_name, last_name"
-                + " from Person";
-
-        assertThat(selectStatement.getSelectStatement()).isEqualTo(expected);
-    }
-
     @Test
     void testLikeCaseInsensitiveWhenPresentTransformWithNull() {
 
@@ -783,7 +647,7 @@ void testNotBetweenTransformWithNull() {
 
         SelectStatementProvider selectStatement = select(id, firstName, lastName)
                 .from(person)
-                .where(age, isNotBetween((Integer) null).and(10).filter(Predicates.bothPresent()).map(i1 -> i1 + 1,  i2 -> i2 + 2))
+                .where(age, isNotBetweenWhenPresent((Integer) null).and(10).map(i1 -> i1 + 1,  i2 -> i2 + 2))
                 .configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true))
                 .build()
                 .render(RenderingStrategies.MYBATIS3);
@@ -810,22 +674,6 @@ void testNotBetweenWhenPresentTransformWithNull() {
         assertThat(selectStatement.getSelectStatement()).isEqualTo(expected);
     }
 
-    @Test
-    void testNotEqualTransformWithNull() {
-
-        SelectStatementProvider selectStatement = select(id, firstName, lastName)
-                .from(person)
-                .where(age, isNotEqualTo((Integer) null).filter(Objects::nonNull).map(i -> i + 1))
-                .configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true))
-                .build()
-                .render(RenderingStrategies.MYBATIS3);
-
-        String expected = "select person_id, first_name, last_name"
-                + " from Person";
-
-        assertThat(selectStatement.getSelectStatement()).isEqualTo(expected);
-    }
-
     @Test
     void testNotEqualWhenPresentTransformWithNull() {
 
@@ -842,38 +690,6 @@ void testNotEqualWhenPresentTransformWithNull() {
         assertThat(selectStatement.getSelectStatement()).isEqualTo(expected);
     }
 
-    @Test
-    void testNotLikeTransformWithNull() {
-
-        SelectStatementProvider selectStatement = select(id, firstName, lastName)
-                .from(person)
-                .where(firstName, isNotLike((String) null).filter(Objects::nonNull).map(SearchUtils::addWildcards))
-                .configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true))
-                .build()
-                .render(RenderingStrategies.MYBATIS3);
-
-        String expected = "select person_id, first_name, last_name"
-                + " from Person";
-
-        assertThat(selectStatement.getSelectStatement()).isEqualTo(expected);
-    }
-
-    @Test
-    void testNotLikeCaseInsensitiveTransformWithNull() {
-
-        SelectStatementProvider selectStatement = select(id, firstName, lastName)
-                .from(person)
-                .where(firstName, isNotLikeCaseInsensitive((String) null).filter(Objects::nonNull).map(SearchUtils::addWildcards))
-                .configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true))
-                .build()
-                .render(RenderingStrategies.MYBATIS3);
-
-        String expected = "select person_id, first_name, last_name"
-                + " from Person";
-
-        assertThat(selectStatement.getSelectStatement()).isEqualTo(expected);
-    }
-
     @Test
     void testNotLikeCaseInsensitiveWhenPresentTransformWithNull() {
 
diff --git a/src/test/java/issues/gh105/PersonDynamicSqlSupport.java b/src/test/java/issues/gh105/PersonDynamicSqlSupport.java
index 8c94a0301..3b33c6591 100644
--- a/src/test/java/issues/gh105/PersonDynamicSqlSupport.java
+++ b/src/test/java/issues/gh105/PersonDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/issues/gh105/SearchUtils.java b/src/test/java/issues/gh105/SearchUtils.java
index 759543dc2..53dee4fdd 100644
--- a/src/test/java/issues/gh105/SearchUtils.java
+++ b/src/test/java/issues/gh105/SearchUtils.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/issues/gh324/Issue324Test.java b/src/test/java/issues/gh324/Issue324Test.java
index 28ec52d35..732d318c0 100644
--- a/src/test/java/issues/gh324/Issue324Test.java
+++ b/src/test/java/issues/gh324/Issue324Test.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/issues/gh324/NameRecord.java b/src/test/java/issues/gh324/NameRecord.java
index 6f6b1b9c6..a00286095 100644
--- a/src/test/java/issues/gh324/NameRecord.java
+++ b/src/test/java/issues/gh324/NameRecord.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/issues/gh324/NameService.java b/src/test/java/issues/gh324/NameService.java
index 564cd221b..d7de92076 100644
--- a/src/test/java/issues/gh324/NameService.java
+++ b/src/test/java/issues/gh324/NameService.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/issues/gh324/NameTableDynamicSqlSupport.java b/src/test/java/issues/gh324/NameTableDynamicSqlSupport.java
index f838814e1..dfce9811b 100644
--- a/src/test/java/issues/gh324/NameTableDynamicSqlSupport.java
+++ b/src/test/java/issues/gh324/NameTableDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/issues/gh324/NameTableMapper.java b/src/test/java/issues/gh324/NameTableMapper.java
index 1ba3d0428..76723ac73 100644
--- a/src/test/java/issues/gh324/NameTableMapper.java
+++ b/src/test/java/issues/gh324/NameTableMapper.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -49,9 +49,9 @@ default Optional<NameRecord> selectOne(SelectDSLCompleter completer) {
         return MyBatis3Utils.selectOne(this::selectOne, selectList, nameTable, completer);
     }
 
-    default Optional<NameRecord> selectByPrimaryKey(Integer id_) {
+    default Optional<NameRecord> selectByPrimaryKey(Integer recordId) {
         return selectOne(c ->
-                c.where(id, isEqualTo(id_))
+                c.where(id, isEqualTo(recordId))
         );
     }
 
diff --git a/src/test/java/issues/gh324/ObservableCache.java b/src/test/java/issues/gh324/ObservableCache.java
index de7ac3f1c..d52a2bcd0 100644
--- a/src/test/java/issues/gh324/ObservableCache.java
+++ b/src/test/java/issues/gh324/ObservableCache.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/issues/gh324/TestUtils.java b/src/test/java/issues/gh324/TestUtils.java
index cda71a260..23c191686 100644
--- a/src/test/java/issues/gh324/TestUtils.java
+++ b/src/test/java/issues/gh324/TestUtils.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/issues/gh324/spring/SpringNameService.java b/src/test/java/issues/gh324/spring/SpringNameService.java
index a35af4e58..9fbfdf03f 100644
--- a/src/test/java/issues/gh324/spring/SpringNameService.java
+++ b/src/test/java/issues/gh324/spring/SpringNameService.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/issues/gh324/spring/SpringTransactionTest.java b/src/test/java/issues/gh324/spring/SpringTransactionTest.java
index 791ce8485..20922d1d1 100644
--- a/src/test/java/issues/gh324/spring/SpringTransactionTest.java
+++ b/src/test/java/issues/gh324/spring/SpringTransactionTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/issues/gh324/spring/TestConfiguration.java b/src/test/java/issues/gh324/spring/TestConfiguration.java
index 8eaa5e0e8..8045963c4 100644
--- a/src/test/java/issues/gh324/spring/TestConfiguration.java
+++ b/src/test/java/issues/gh324/spring/TestConfiguration.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/issues/gh430/NoInitialConditionTest.java b/src/test/java/issues/gh430/NoInitialConditionTest.java
index b9104d43e..9add2bf6e 100644
--- a/src/test/java/issues/gh430/NoInitialConditionTest.java
+++ b/src/test/java/issues/gh430/NoInitialConditionTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,7 +19,7 @@
 import static org.mybatis.dynamic.sql.SqlBuilder.*;
 import static org.mybatis.dynamic.sql.subselect.FooDynamicSqlSupport.*;
 
-import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Date;
 import java.util.List;
 
@@ -32,7 +32,7 @@ class NoInitialConditionTest {
 
     @Test
     void testNoInitialConditionEmptyList() {
-        List<AndOrCriteriaGroup> criteria = new ArrayList<>();
+        List<AndOrCriteriaGroup> criteria = Collections.emptyList();
 
         SelectStatementProvider selectStatement = buildSelectStatement(criteria);
 
@@ -43,8 +43,7 @@ void testNoInitialConditionEmptyList() {
 
     @Test
     void testNoInitialConditionSingleSub() {
-        List<AndOrCriteriaGroup> criteria = new ArrayList<>();
-        criteria.add(or(column2, isEqualTo(3)));
+        List<AndOrCriteriaGroup> criteria = List.of(or(column2, isEqualTo(3)));
 
         SelectStatementProvider selectStatement = buildSelectStatement(criteria);
 
@@ -56,10 +55,10 @@ void testNoInitialConditionSingleSub() {
 
     @Test
     void testNoInitialConditionMultipleSubs() {
-        List<AndOrCriteriaGroup> criteria = new ArrayList<>();
-        criteria.add(or(column2, isEqualTo(3)));
-        criteria.add(or(column2, isEqualTo(4)));
-        criteria.add(or(column2, isEqualTo(5)));
+        List<AndOrCriteriaGroup> criteria = List.of(
+            or(column2, isEqualTo(3)),
+            or(column2, isEqualTo(4)),
+            or(column2, isEqualTo(5)));
 
         SelectStatementProvider selectStatement = buildSelectStatement(criteria);
 
@@ -71,10 +70,10 @@ void testNoInitialConditionMultipleSubs() {
 
     @Test
     void testNoInitialConditionWhereMultipleSubs() {
-        List<AndOrCriteriaGroup> criteria = new ArrayList<>();
-        criteria.add(or(column2, isEqualTo(3)));
-        criteria.add(or(column2, isEqualTo(4)));
-        criteria.add(or(column2, isEqualTo(5)));
+        List<AndOrCriteriaGroup> criteria = List.of(
+            or(column2, isEqualTo(3)),
+            or(column2, isEqualTo(4)),
+            or(column2, isEqualTo(5)));
 
         SelectStatementProvider selectStatement = select(column1, column2)
                 .from(foo)
@@ -90,10 +89,10 @@ void testNoInitialConditionWhereMultipleSubs() {
 
     @Test
     void testNoInitialConditionWhereNotMultipleSubs() {
-        List<AndOrCriteriaGroup> criteria = new ArrayList<>();
-        criteria.add(or(column2, isEqualTo(3)));
-        criteria.add(or(column2, isEqualTo(4)));
-        criteria.add(or(column2, isEqualTo(5)));
+        List<AndOrCriteriaGroup> criteria = List.of(
+                or(column2, isEqualTo(3)),
+                or(column2, isEqualTo(4)),
+                or(column2, isEqualTo(5)));
 
         SelectStatementProvider selectStatement = select(column1, column2)
                 .from(foo)
@@ -110,10 +109,10 @@ void testNoInitialConditionWhereNotMultipleSubs() {
 
     @Test
     void testNoInitialConditionWhereGroupMultipleSubs() {
-        List<AndOrCriteriaGroup> criteria = new ArrayList<>();
-        criteria.add(or(column2, isEqualTo(3)));
-        criteria.add(or(column2, isEqualTo(4)));
-        criteria.add(or(column2, isEqualTo(5)));
+        List<AndOrCriteriaGroup> criteria = List.of(
+                or(column2, isEqualTo(3)),
+                or(column2, isEqualTo(4)),
+                or(column2, isEqualTo(5)));
 
         SelectStatementProvider selectStatement = select(column1, column2)
                 .from(foo)
@@ -130,10 +129,10 @@ void testNoInitialConditionWhereGroupMultipleSubs() {
 
     @Test
     void testNoInitialConditionWhereCCAndMultipleSubs() {
-        List<AndOrCriteriaGroup> criteria = new ArrayList<>();
-        criteria.add(or(column2, isEqualTo(3)));
-        criteria.add(or(column2, isEqualTo(4)));
-        criteria.add(or(column2, isEqualTo(5)));
+        List<AndOrCriteriaGroup> criteria = List.of(
+                or(column2, isEqualTo(3)),
+                or(column2, isEqualTo(4)),
+                or(column2, isEqualTo(5)));
 
         SelectStatementProvider selectStatement = select(column1, column2)
                 .from(foo)
@@ -149,10 +148,10 @@ void testNoInitialConditionWhereCCAndMultipleSubs() {
 
     @Test
     void testNoInitialConditionWhereCCOrMultipleSubs() {
-        List<AndOrCriteriaGroup> criteria = new ArrayList<>();
-        criteria.add(or(column2, isEqualTo(3)));
-        criteria.add(or(column2, isEqualTo(4)));
-        criteria.add(or(column2, isEqualTo(5)));
+        List<AndOrCriteriaGroup> criteria = List.of(
+                or(column2, isEqualTo(3)),
+                or(column2, isEqualTo(4)),
+                or(column2, isEqualTo(5)));
 
         SelectStatementProvider selectStatement = select(column1, column2)
                 .from(foo)
@@ -168,10 +167,10 @@ void testNoInitialConditionWhereCCOrMultipleSubs() {
 
     @Test
     void testNoInitialConditionWhereOrMultipleSubs() {
-        List<AndOrCriteriaGroup> criteria = new ArrayList<>();
-        criteria.add(or(column2, isEqualTo(3)));
-        criteria.add(or(column2, isEqualTo(4)));
-        criteria.add(or(column2, isEqualTo(5)));
+        List<AndOrCriteriaGroup> criteria = List.of(
+                or(column2, isEqualTo(3)),
+                or(column2, isEqualTo(4)),
+                or(column2, isEqualTo(5)));
 
         SelectStatementProvider selectStatement = select(column1, column2)
                 .from(foo)
diff --git a/src/test/java/issues/gh655/Gh655Test.java b/src/test/java/issues/gh655/Gh655Test.java
index 457936dc9..e1eb4578f 100644
--- a/src/test/java/issues/gh655/Gh655Test.java
+++ b/src/test/java/issues/gh655/Gh655Test.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/issues/lhg142/Issue142Test.java b/src/test/java/issues/lhg142/Issue142Test.java
index e290e37d0..4f2ffe60c 100644
--- a/src/test/java/issues/lhg142/Issue142Test.java
+++ b/src/test/java/issues/lhg142/Issue142Test.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/issues/lhg142/MyMarkDynamicSqlSupport.java b/src/test/java/issues/lhg142/MyMarkDynamicSqlSupport.java
index c543ea9f0..b4eae4a83 100644
--- a/src/test/java/issues/lhg142/MyMarkDynamicSqlSupport.java
+++ b/src/test/java/issues/lhg142/MyMarkDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/issues/lhg142/Page.java b/src/test/java/issues/lhg142/Page.java
index 34fe70874..dd0b72b97 100644
--- a/src/test/java/issues/lhg142/Page.java
+++ b/src/test/java/issues/lhg142/Page.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/org/mybatis/dynamic/sql/BindableColumnTest.java b/src/test/java/org/mybatis/dynamic/sql/BindableColumnTest.java
index cc962986a..bc295b741 100644
--- a/src/test/java/org/mybatis/dynamic/sql/BindableColumnTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/BindableColumnTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/org/mybatis/dynamic/sql/InvalidSQLTest.java b/src/test/java/org/mybatis/dynamic/sql/InvalidSQLTest.java
index 4c7afc0b0..8e8981567 100644
--- a/src/test/java/org/mybatis/dynamic/sql/InvalidSQLTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/InvalidSQLTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -17,22 +17,17 @@
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
-import static org.mybatis.dynamic.sql.SqlBuilder.insert;
-import static org.mybatis.dynamic.sql.SqlBuilder.insertInto;
-import static org.mybatis.dynamic.sql.SqlBuilder.select;
-import static org.mybatis.dynamic.sql.SqlBuilder.update;
-import static org.mybatis.dynamic.sql.SqlBuilder.value;
+import static org.mybatis.dynamic.sql.SqlBuilder.*;
 
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.MissingResourceException;
 import java.util.Optional;
 
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.Test;
 import org.mybatis.dynamic.sql.common.OrderByModel;
 import org.mybatis.dynamic.sql.configuration.StatementConfiguration;
-import org.mybatis.dynamic.sql.exception.DynamicSqlException;
 import org.mybatis.dynamic.sql.exception.InvalidSqlException;
 import org.mybatis.dynamic.sql.insert.BatchInsertModel;
 import org.mybatis.dynamic.sql.insert.GeneralInsertModel;
@@ -41,7 +36,6 @@
 import org.mybatis.dynamic.sql.insert.MultiRowInsertModel;
 import org.mybatis.dynamic.sql.render.RenderingContext;
 import org.mybatis.dynamic.sql.render.RenderingStrategies;
-import org.mybatis.dynamic.sql.render.TableAliasCalculator;
 import org.mybatis.dynamic.sql.select.GroupByModel;
 import org.mybatis.dynamic.sql.select.PagingModel;
 import org.mybatis.dynamic.sql.select.QueryExpressionModel;
@@ -50,7 +44,6 @@
 import org.mybatis.dynamic.sql.select.join.JoinSpecification;
 import org.mybatis.dynamic.sql.select.join.JoinType;
 import org.mybatis.dynamic.sql.select.render.FetchFirstPagingModelRenderer;
-import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
 import org.mybatis.dynamic.sql.update.UpdateModel;
 import org.mybatis.dynamic.sql.util.InternalError;
 import org.mybatis.dynamic.sql.util.Messages;
@@ -115,8 +108,7 @@ void testInvalidMultipleInsertStatementNoRecords() {
 
     @Test
     void testInvalidMultipleInsertStatementNoMappings() {
-        List<TestRow> records = new ArrayList<>();
-        records.add(new TestRow());
+        List<TestRow> records = List.of(new TestRow());
 
         MultiRowInsertModel.Builder<TestRow> builder = new MultiRowInsertModel.Builder<TestRow>()
                 .withRecords(records)
@@ -137,8 +129,7 @@ void testInvalidBatchInsertStatementNoRecords() {
 
     @Test
     void testInvalidBatchInsertStatementNoMappings() {
-        List<TestRow> records = new ArrayList<>();
-        records.add(new TestRow());
+        List<TestRow> records = List.of(new TestRow());
 
         BatchInsertModel.Builder<TestRow> builder = new BatchInsertModel.Builder<TestRow>()
                 .withRecords(records)
@@ -268,69 +259,63 @@ void testInvalidValueAlias() {
     }
 
     @Test
-    void testBadColumn() {
-        SelectModel selectModel = select(new BadCount<>()).from(person).build();
-        assertThatExceptionOfType(DynamicSqlException.class)
-                .isThrownBy(() -> selectModel.render(RenderingStrategies.MYBATIS3))
-                .withMessage(Messages.getString("ERROR.36"));
+    void testInvalidDoubleForUpdate() {
+        var dsl = select(id).from(person).limit(2).forUpdate();
+        assertThatExceptionOfType(InvalidSqlException.class).isThrownBy(dsl::forUpdate)
+        .withMessage(Messages.getString("ERROR.48"));
     }
 
     @Test
-    void testDeprecatedColumn() {
-        SelectStatementProvider selectStatement = select(new DeprecatedCount<>())
-                .from(person)
-                .build()
-                .render(RenderingStrategies.MYBATIS3);
-        assertThat(selectStatement.getSelectStatement()).isEqualTo("select count(*) from person");
+    void testInvalidDoubleForShare() {
+        var dsl = select(id).from(person).offset(2).forShare();
+        assertThatExceptionOfType(InvalidSqlException.class).isThrownBy(dsl::forShare)
+                .withMessage(Messages.getString("ERROR.48"));
     }
 
-    static class TestRow {
-        private Integer id;
-
-        public Integer getId() {
-            return id;
-        }
-
-        public void setId(Integer id) {
-            this.id = id;
-        }
+    @Test
+    void testInvalidDoubleForKeyShare() {
+        var dsl = select(id).from(person).forKeyShare();
+        assertThatExceptionOfType(InvalidSqlException.class).isThrownBy(dsl::forKeyShare)
+                .withMessage(Messages.getString("ERROR.48"));
     }
 
-    static class BadCount<T> implements BindableColumn<T> {
-        private String alias;
+    @Test
+    void testInvalidDoubleForNoKeyUpdate() {
+        var dsl = select(id).from(person).where(id, isEqualTo(1)).forNoKeyUpdate();
+        assertThatExceptionOfType(InvalidSqlException.class).isThrownBy(dsl::forNoKeyUpdate)
+                .withMessage(Messages.getString("ERROR.48"));
+    }
 
-        @Override
-        public Optional<String> alias() {
-            return Optional.ofNullable(alias);
-        }
+    @Test
+    void testInvalidDoubleForNoKeyUpdateAfterJoin() {
+        var dsl = select(id).from(person).join(person).on(id, isEqualTo(id)).skipLocked();
+        assertThatExceptionOfType(InvalidSqlException.class).isThrownBy(dsl::skipLocked)
+                .withMessage(Messages.getString("ERROR.49"));
+    }
 
-        @Override
-        public BindableColumn<T> as(String alias) {
-            BadCount<T> newCount = new BadCount<>();
-            newCount.alias = alias;
-            return newCount;
-        }
+    @Test
+    void testInvalidDoubleForNoKeyUpdateAfterGroupBy() {
+        var dsl = select(id).from(person).groupBy(id).nowait();
+        assertThatExceptionOfType(InvalidSqlException.class).isThrownBy(dsl::nowait)
+                .withMessage(Messages.getString("ERROR.49"));
     }
 
-    static class DeprecatedCount<T> implements BindableColumn<T> {
-        private String alias;
+    @Test
+    void testInvalidDoubleForNoKeyUpdateAfterHaving() {
+        var dsl = select(id).from(person).groupBy(id).having(id, isEqualTo(2)).nowait();
+        assertThatExceptionOfType(InvalidSqlException.class).isThrownBy(dsl::nowait)
+                .withMessage(Messages.getString("ERROR.49"));
+    }
 
-        @Override
-        @Deprecated
-        public String renderWithTableAlias(TableAliasCalculator tableAliasCalculator) {
-            return "count(*)";
-        }
+    static class TestRow {
+        private @Nullable Integer id;
 
-        @Override
-        public Optional<String> alias() {
-            return Optional.ofNullable(alias);
+        public @Nullable Integer getId() {
+            return id;
         }
 
-        @Override
-        public BindableColumn<T> as(String alias) {
-            DeprecatedCount<T> newCount = new DeprecatedCount<>();
-            newCount.alias = alias;
-            return newCount;
+        public void setId(Integer id) {
+            this.id = id;
         }
     }
 }
diff --git a/src/test/java/org/mybatis/dynamic/sql/SqlTableTest.java b/src/test/java/org/mybatis/dynamic/sql/SqlTableTest.java
deleted file mode 100644
index 32d336572..000000000
--- a/src/test/java/org/mybatis/dynamic/sql/SqlTableTest.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- *    Copyright 2016-2024 the original author or authors.
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *       https://www.apache.org/licenses/LICENSE-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 static org.assertj.core.api.Assertions.assertThat;
-
-import java.util.Optional;
-import java.util.function.Supplier;
-
-import org.junit.jupiter.api.Test;
-
-class SqlTableTest {
-
-    private static final String NAME_PROPERTY = "nameProperty";
-
-    @Test
-    void testFullName() {
-        SqlTable table = new SqlTable("my_table");
-        assertThat(table.tableNameAtRuntime()).isEqualTo("my_table");
-    }
-
-    @Test
-    void testFullNameSupplier() {
-
-        System.setProperty(NAME_PROPERTY, "my_table");
-        SqlTable table = new SqlTable(SqlTableTest::namePropertyReader);
-        assertThat(table.tableNameAtRuntime()).isEqualTo("my_table");
-        System.clearProperty(NAME_PROPERTY);
-    }
-
-    @Test
-    void testSchemaSupplierEmpty() {
-        SqlTable table = new SqlTable(Optional::empty, "my_table");
-        assertThat(table.tableNameAtRuntime()).isEqualTo("my_table");
-    }
-
-    @Test
-    void testSchemaSupplierWithValue() {
-        SqlTable table = new SqlTable(() -> Optional.of("my_schema"), "my_table");
-        assertThat(table.tableNameAtRuntime()).isEqualTo("my_schema.my_table");
-    }
-
-    @Test
-    void testSingletonSchemaSupplier() {
-        SqlTable table = new SqlTable(MySchemaSupplier.instance(), "my_table");
-        assertThat(table.tableNameAtRuntime()).isEqualTo("first_schema.my_table");
-    }
-
-    @Test
-    void testThatSchemaSupplierDoesDelay() {
-        MySchemaSupplier schemaSupplier = new MySchemaSupplier();
-        SqlTable table = new SqlTable(schemaSupplier, "my_table");
-        assertThat(table.tableNameAtRuntime()).isEqualTo("first_schema.my_table");
-
-        schemaSupplier.setFirst(false);
-        assertThat(table.tableNameAtRuntime()).isEqualTo("second_schema.my_table");
-    }
-
-    @Test
-    void testCatalogAndSchemaSupplierEmpty() {
-        SqlTable table = new SqlTable(Optional::empty, Optional::empty, "my_table");
-        assertThat(table.tableNameAtRuntime()).isEqualTo("my_table");
-    }
-
-    @Test
-    void testCatalogSupplierWithValue() {
-        SqlTable table = new SqlTable(() -> Optional.of("my_catalog"), Optional::empty, "my_table");
-        assertThat(table.tableNameAtRuntime()).isEqualTo("my_catalog..my_table");
-    }
-
-    @Test
-    void testThatCatalogSupplierDoesDelay() {
-        MyCatalogSupplier catalogSupplier = new MyCatalogSupplier();
-        SqlTable table = new SqlTable(catalogSupplier, Optional::empty, "my_table");
-        assertThat(table.tableNameAtRuntime()).isEqualTo("first_catalog..my_table");
-
-        catalogSupplier.setFirst(false);
-        assertThat(table.tableNameAtRuntime()).isEqualTo("second_catalog..my_table");
-    }
-
-    @Test
-    void testThatCatalogSupplierAndSchemaSupplierBothDelay() {
-        MyCatalogSupplier catalogSupplier = new MyCatalogSupplier();
-        MySchemaSupplier schemaSupplier = new MySchemaSupplier();
-        SqlTable table = new SqlTable(catalogSupplier, schemaSupplier, "my_table");
-        assertThat(table.tableNameAtRuntime()).isEqualTo("first_catalog.first_schema.my_table");
-
-        catalogSupplier.setFirst(false);
-        assertThat(table.tableNameAtRuntime()).isEqualTo("second_catalog.first_schema.my_table");
-
-        catalogSupplier.setFirst(true);
-        schemaSupplier.setFirst(false);
-        assertThat(table.tableNameAtRuntime()).isEqualTo("first_catalog.second_schema.my_table");
-
-        catalogSupplier.setFirst(false);
-        assertThat(table.tableNameAtRuntime()).isEqualTo("second_catalog.second_schema.my_table");
-
-        catalogSupplier.setEmpty(true);
-        assertThat(table.tableNameAtRuntime()).isEqualTo("second_schema.my_table");
-
-        schemaSupplier.setEmpty(true);
-        assertThat(table.tableNameAtRuntime()).isEqualTo("my_table");
-
-        catalogSupplier.setEmpty(false);
-        assertThat(table.tableNameAtRuntime()).isEqualTo("second_catalog..my_table");
-    }
-
-    private static String namePropertyReader() {
-        return System.getProperty(NAME_PROPERTY);
-    }
-
-    static class MySchemaSupplier implements Supplier<Optional<String>> {
-        private static final MySchemaSupplier instance = new MySchemaSupplier();
-
-        static MySchemaSupplier instance() {
-            return instance;
-        }
-
-        private boolean first = true;
-        private boolean empty;
-
-        void setFirst(boolean first) {
-            this.first = first;
-        }
-
-        void setEmpty(boolean empty) {
-            this.empty = empty;
-        }
-
-        @Override
-        public Optional<String> get() {
-            if (empty) {
-                return Optional.empty();
-            }
-
-            if (first) {
-                return Optional.of("first_schema");
-            } else {
-                return Optional.of("second_schema");
-            }
-        }
-    }
-
-    static class MyCatalogSupplier implements Supplier<Optional<String>> {
-        private boolean first = true;
-        private boolean empty;
-
-        void setFirst(boolean first) {
-            this.first = first;
-        }
-
-        void setEmpty(boolean empty) {
-            this.empty = empty;
-        }
-
-        @Override
-        public Optional<String> get() {
-            if (empty) {
-                return Optional.empty();
-            }
-
-            if (first) {
-                return Optional.of("first_catalog");
-            } else {
-                return Optional.of("second_catalog");
-            }
-        }
-    }
-}
diff --git a/src/test/java/org/mybatis/dynamic/sql/StatementConfigurationTest.java b/src/test/java/org/mybatis/dynamic/sql/StatementConfigurationTest.java
index bfc76b27f..5112d8de9 100644
--- a/src/test/java/org/mybatis/dynamic/sql/StatementConfigurationTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/StatementConfigurationTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/org/mybatis/dynamic/sql/configuration/GlobalConfigurationTest.java b/src/test/java/org/mybatis/dynamic/sql/configuration/GlobalConfigurationTest.java
index 0b929f3fe..9f9998780 100644
--- a/src/test/java/org/mybatis/dynamic/sql/configuration/GlobalConfigurationTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/configuration/GlobalConfigurationTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/org/mybatis/dynamic/sql/delete/DeleteStatementTest.java b/src/test/java/org/mybatis/dynamic/sql/delete/DeleteStatementTest.java
index c247fd8f5..76edac9bb 100644
--- a/src/test/java/org/mybatis/dynamic/sql/delete/DeleteStatementTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/delete/DeleteStatementTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/org/mybatis/dynamic/sql/insert/GeneralInsertStatementTest.java b/src/test/java/org/mybatis/dynamic/sql/insert/GeneralInsertStatementTest.java
index 1063a39a7..22813df69 100644
--- a/src/test/java/org/mybatis/dynamic/sql/insert/GeneralInsertStatementTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/insert/GeneralInsertStatementTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -85,14 +85,12 @@ void testInsertStatementBuilderWithConstants() {
 
     @Test
     void testSelectiveInsertStatementBuilder() {
-        Integer myId = null;
-        String myFirstName = null;
         String myLastName = "jones";
         String myOccupation = "dino driver";
 
         GeneralInsertStatementProvider insertStatement = insertInto(foo)
-                .set(id).toValueWhenPresent(() -> myId)
-                .set(firstName).toValueWhenPresent(myFirstName)
+                .set(id).toValueWhenPresent(() -> null)
+                .set(firstName).toValueWhenPresent((String) null)
                 .set(lastName).toValueWhenPresent(() -> myLastName)
                 .set(occupation).toValueWhenPresent(myOccupation)
                 .build()
diff --git a/src/test/java/org/mybatis/dynamic/sql/insert/InsertStatementTest.java b/src/test/java/org/mybatis/dynamic/sql/insert/InsertStatementTest.java
index 350532cb2..29249317b 100644
--- a/src/test/java/org/mybatis/dynamic/sql/insert/InsertStatementTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/insert/InsertStatementTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -20,6 +20,7 @@
 
 import java.sql.JDBCType;
 
+import org.jspecify.annotations.Nullable;
 import org.junit.jupiter.api.Test;
 import org.mybatis.dynamic.sql.SqlColumn;
 import org.mybatis.dynamic.sql.SqlTable;
@@ -37,9 +38,7 @@ class InsertStatementTest {
     @Test
     void testFullInsertStatementBuilder() {
 
-        TestRecord row = new TestRecord();
-        row.setLastName("jones");
-        row.setOccupation("dino driver");
+        TestRecord row = new TestRecord(null, null, "jones", "dino driver");
 
         InsertStatementProvider<TestRecord> insertStatement = insert(row)
                 .into(foo)
@@ -97,16 +96,14 @@ void testInsertStatementBuilderWithConstants() {
 
     @Test
     void testSelectiveInsertStatementBuilder() {
-        TestRecord row = new TestRecord();
-        row.setLastName("jones");
-        row.setOccupation("dino driver");
+        TestRecord row = new TestRecord(null, null, "jones", "dino driver");
 
         InsertStatementProvider<TestRecord> insertStatement = insert(row)
                 .into(foo)
-                .map(id).toPropertyWhenPresent("id", row::getId)
-                .map(firstName).toPropertyWhenPresent("firstName", row::getFirstName)
-                .map(lastName).toPropertyWhenPresent("lastName", row::getLastName)
-                .map(occupation).toPropertyWhenPresent("occupation", row::getOccupation)
+                .map(id).toPropertyWhenPresent("id", row::id)
+                .map(firstName).toPropertyWhenPresent("firstName", row::firstName)
+                .map(lastName).toPropertyWhenPresent("lastName", row::lastName)
+                .map(occupation).toPropertyWhenPresent("occupation", row::occupation)
                 .build()
                 .render(RenderingStrategies.MYBATIS3);
 
@@ -115,60 +112,9 @@ void testSelectiveInsertStatementBuilder() {
         assertThat(insertStatement.getInsertStatement()).isEqualTo(expected);
     }
 
-    @Test
-    void testDeprecatedMethod() {
-        TestRecord row = new TestRecord();
-        row.setLastName("jones");
-        row.setOccupation("dino driver");
-
-        InsertStatementProvider<TestRecord> insertStatement = insert(row)
-                .into(foo)
-                .map(id).toPropertyWhenPresent("id", row::getId)
-                .map(firstName).toPropertyWhenPresent("firstName", row::getFirstName)
-                .map(lastName).toPropertyWhenPresent("lastName", row::getLastName)
-                .map(occupation).toPropertyWhenPresent("occupation", row::getOccupation)
-                .build()
-                .render(RenderingStrategies.MYBATIS3);
-
-        assertThat(insertStatement.getRow()).isEqualTo(insertStatement.getRecord());
-    }
-
-    static class TestRecord {
-        private Integer id;
-        private String firstName;
-        private String lastName;
-        private String occupation;
-
-        Integer getId() {
-            return id;
-        }
-
-        void setId(Integer id) {
-            this.id = id;
-        }
-
-        String getFirstName() {
-            return firstName;
-        }
-
-        void setFirstName(String firstName) {
-            this.firstName = firstName;
-        }
-
-        String getLastName() {
-            return lastName;
-        }
-
-        void setLastName(String lastName) {
-            this.lastName = lastName;
-        }
-
-        String getOccupation() {
-            return occupation;
-        }
-
-        void setOccupation(String occupation) {
-            this.occupation = occupation;
+    record TestRecord (@Nullable Integer id, @Nullable String firstName, @Nullable String lastName, @Nullable String occupation) {
+        TestRecord() {
+            this(null, null, null, null);
         }
     }
 }
diff --git a/src/test/java/org/mybatis/dynamic/sql/insert/MapToRowTest.java b/src/test/java/org/mybatis/dynamic/sql/insert/MapToRowTest.java
index cd4871f14..d17811fe1 100644
--- a/src/test/java/org/mybatis/dynamic/sql/insert/MapToRowTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/insert/MapToRowTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -20,7 +20,6 @@
 import static org.mybatis.dynamic.sql.SqlBuilder.insertMultiple;
 
 import java.sql.JDBCType;
-import java.util.ArrayList;
 import java.util.List;
 
 import org.junit.jupiter.api.Test;
@@ -37,10 +36,10 @@ class MapToRowTest {
 
     @Test
     void testBasicInsertMultipleWithMyBatis() {
-        List<Record> records = new ArrayList<>();
-        records.add(new Record(33, 1));
-        records.add(new Record(33, 2));
-        records.add(new Record(33, 3));
+        List<Record> records = List.of(
+                new Record(33, 1),
+                new Record(33, 2),
+                new Record(33, 3));
 
         MultiRowInsertStatementProvider<Record> insertStatement = insertMultiple(records)
                 .into(foo)
@@ -55,10 +54,10 @@ void testBasicInsertMultipleWithMyBatis() {
 
     @Test
     void testBasicInsertMultipleWithSpring() {
-        List<Record> records = new ArrayList<>();
-        records.add(new Record(33, 1));
-        records.add(new Record(33, 2));
-        records.add(new Record(33, 3));
+        List<Record> records = List.of(
+                new Record(33, 1),
+                new Record(33, 2),
+                new Record(33, 3));
 
         MultiRowInsertStatementProvider<Record> insertStatement = insertMultiple(records)
                 .into(foo)
@@ -73,10 +72,7 @@ void testBasicInsertMultipleWithSpring() {
 
     @Test
     void testBasicInsertMultipleRowMappingWithMyBatis() {
-        List<Integer> integers = new ArrayList<>();
-        integers.add(1);
-        integers.add(2);
-        integers.add(3);
+        List<Integer> integers = List.of(1, 2, 3);
 
         MultiRowInsertStatementProvider<Integer> insertStatement = insertMultiple(integers)
                 .into(foo)
@@ -91,10 +87,7 @@ void testBasicInsertMultipleRowMappingWithMyBatis() {
 
     @Test
     void testBasicInsertMultipleRowMappingWithSpring() {
-        List<Integer> integers = new ArrayList<>();
-        integers.add(1);
-        integers.add(2);
-        integers.add(3);
+        List<Integer> integers = List.of(1, 2, 3);
 
         MultiRowInsertStatementProvider<Integer> insertStatement = insertMultiple(integers)
                 .into(foo)
@@ -109,10 +102,10 @@ void testBasicInsertMultipleRowMappingWithSpring() {
 
     @Test
     void testBatchInsertWithMyBatis() {
-        List<Record> records = new ArrayList<>();
-        records.add(new Record(33, 1));
-        records.add(new Record(33, 2));
-        records.add(new Record(33, 3));
+        List<Record> records = List.of(
+                new Record(33, 1),
+                new Record(33, 2),
+                new Record(33, 3));
 
         BatchInsert<Record> batchInsert = insertBatch(records)
                 .into(foo)
@@ -127,10 +120,10 @@ void testBatchInsertWithMyBatis() {
 
     @Test
     void testBatchInsertWithSpring() {
-        List<Record> records = new ArrayList<>();
-        records.add(new Record(33, 1));
-        records.add(new Record(33, 2));
-        records.add(new Record(33, 3));
+        List<Record> records = List.of(
+                new Record(33, 1),
+                new Record(33, 2),
+                new Record(33, 3));
 
         BatchInsert<Record> batchInsert = insertBatch(records)
                 .into(foo)
@@ -145,10 +138,7 @@ void testBatchInsertWithSpring() {
 
     @Test
     void testBatchInsertRowMappingWithMyBatis() {
-        List<Integer> integers = new ArrayList<>();
-        integers.add(1);
-        integers.add(2);
-        integers.add(3);
+        List<Integer> integers = List.of(1, 2, 3);
 
         BatchInsert<Integer> batchInsert = insertBatch(integers)
                 .into(foo)
@@ -163,10 +153,7 @@ void testBatchInsertRowMappingWithMyBatis() {
 
     @Test
     void testBatchInsertRowMappingWithSpring() {
-        List<Integer> integers = new ArrayList<>();
-        integers.add(1);
-        integers.add(2);
-        integers.add(3);
+        List<Integer> integers = List.of(1, 2, 3);
 
         BatchInsert<Integer> batchInsert = insertBatch(integers)
                 .into(foo)
@@ -179,13 +166,5 @@ void testBatchInsertRowMappingWithSpring() {
         assertThat(batchInsert.getInsertStatementSQL()).isEqualTo(expected);
     }
 
-    static class Record {
-        public Record(Integer id1, Integer id2) {
-            this.id1 = id1;
-            this.id2 = id2;
-        }
-
-        public Integer id1;
-        public Integer id2;
-    }
+    record Record(Integer id1, Integer id2) { }
 }
diff --git a/src/test/java/org/mybatis/dynamic/sql/insert/render/FieldAndValueCollectorTest.java b/src/test/java/org/mybatis/dynamic/sql/insert/render/FieldAndValueCollectorTest.java
index ffc8b7d8f..75f414d5d 100644
--- a/src/test/java/org/mybatis/dynamic/sql/insert/render/FieldAndValueCollectorTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/insert/render/FieldAndValueCollectorTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/org/mybatis/dynamic/sql/mybatis3/CriterionRendererTest.java b/src/test/java/org/mybatis/dynamic/sql/mybatis3/CriterionRendererTest.java
index ef82f3322..ca520cc91 100644
--- a/src/test/java/org/mybatis/dynamic/sql/mybatis3/CriterionRendererTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/mybatis3/CriterionRendererTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/org/mybatis/dynamic/sql/mybatis3/InsertStatementTest.java b/src/test/java/org/mybatis/dynamic/sql/mybatis3/InsertStatementTest.java
index 471c9ef3e..5e0ef7f7b 100644
--- a/src/test/java/org/mybatis/dynamic/sql/mybatis3/InsertStatementTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/mybatis3/InsertStatementTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/org/mybatis/dynamic/sql/mybatis3/SelectStatementTest.java b/src/test/java/org/mybatis/dynamic/sql/mybatis3/SelectStatementTest.java
index dae3b9585..a7cecc2f1 100644
--- a/src/test/java/org/mybatis/dynamic/sql/mybatis3/SelectStatementTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/mybatis3/SelectStatementTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/org/mybatis/dynamic/sql/mybatis3/UpdateStatementTest.java b/src/test/java/org/mybatis/dynamic/sql/mybatis3/UpdateStatementTest.java
index 6a6b92dfa..2ee506f38 100644
--- a/src/test/java/org/mybatis/dynamic/sql/mybatis3/UpdateStatementTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/mybatis3/UpdateStatementTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/org/mybatis/dynamic/sql/select/HavingModelTest.java b/src/test/java/org/mybatis/dynamic/sql/select/HavingModelTest.java
index 47d015da2..92ddb02a0 100644
--- a/src/test/java/org/mybatis/dynamic/sql/select/HavingModelTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/select/HavingModelTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/org/mybatis/dynamic/sql/select/SelectStatementTest.java b/src/test/java/org/mybatis/dynamic/sql/select/SelectStatementTest.java
index 19832a415..fe60e6924 100644
--- a/src/test/java/org/mybatis/dynamic/sql/select/SelectStatementTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/select/SelectStatementTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -21,7 +21,6 @@
 import static org.mybatis.dynamic.sql.SqlBuilder.*;
 
 import java.sql.JDBCType;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
@@ -180,9 +179,7 @@ void testOrderByMultipleColumns() {
 
     @Test
     void testOrderByMultipleColumnsWithCollection() {
-        Collection<SortSpecification> orderByColumns = new ArrayList<>();
-        orderByColumns.add(column2.descending());
-        orderByColumns.add(column1);
+        Collection<SortSpecification> orderByColumns = List.of(column2.descending(), column1);
 
         SelectStatementProvider selectStatement = select(column1.as("A_COLUMN1"), column2)
                 .from(table, "a")
@@ -285,18 +282,6 @@ void testGroupBySingleColumn() {
         );
     }
 
-    @Test
-    void testInEmptyList() {
-        List<String> emptyList = Collections.emptyList();
-        SelectStatementProvider selectStatement = select(column1, column3)
-                .from(table, "a")
-                .where(column3, isIn(emptyList))
-                .build()
-                .render(RenderingStrategies.MYBATIS3);
-
-        assertThat(selectStatement.getSelectStatement()).isEqualTo("select a.column1, a.column3 from foo a where a.column3 in ()");
-    }
-
     @Test
     void testNotInEmptyList() {
         List<String> emptyList = Collections.emptyList();
@@ -360,17 +345,6 @@ void testNotInWhenPresentEmptyList() {
         );
     }
 
-    @Test
-    void testNotInCaseInsensitiveEmptyList() {
-        SelectStatementProvider selectStatement = select(column1, column3)
-                .from(table, "a")
-                .where(column3, isNotInCaseInsensitive(Collections.emptyList()))
-                .build()
-                .render(RenderingStrategies.MYBATIS3);
-
-        assertThat(selectStatement.getSelectStatement()).isEqualTo("select a.column1, a.column3 from foo a where upper(a.column3) not in ()");
-    }
-
     @Test
     void testNotInCaseInsensitiveWhenPresentEmptyList() {
         SelectModel selectModel = select(column1, column3)
diff --git a/src/test/java/org/mybatis/dynamic/sql/subselect/FooDynamicSqlSupport.java b/src/test/java/org/mybatis/dynamic/sql/subselect/FooDynamicSqlSupport.java
index 66741d5ce..7398dce58 100644
--- a/src/test/java/org/mybatis/dynamic/sql/subselect/FooDynamicSqlSupport.java
+++ b/src/test/java/org/mybatis/dynamic/sql/subselect/FooDynamicSqlSupport.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/org/mybatis/dynamic/sql/subselect/SubSelectTest.java b/src/test/java/org/mybatis/dynamic/sql/subselect/SubSelectTest.java
index f0835b173..9e4506df8 100644
--- a/src/test/java/org/mybatis/dynamic/sql/subselect/SubSelectTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/subselect/SubSelectTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/org/mybatis/dynamic/sql/update/UpdateStatementTest.java b/src/test/java/org/mybatis/dynamic/sql/update/UpdateStatementTest.java
index 1ad1232eb..061484f2c 100644
--- a/src/test/java/org/mybatis/dynamic/sql/update/UpdateStatementTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/update/UpdateStatementTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/org/mybatis/dynamic/sql/util/ColumnMappingVisitorTest.java b/src/test/java/org/mybatis/dynamic/sql/util/ColumnMappingVisitorTest.java
index 2af6cb3db..2f17910b2 100644
--- a/src/test/java/org/mybatis/dynamic/sql/util/ColumnMappingVisitorTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/util/ColumnMappingVisitorTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -205,8 +205,8 @@ void testThatUpdateVisitorErrorsForRowMapping() {
     }
 
     private static class TestTable extends SqlTable {
-        public SqlColumn<Integer> id;
-        public SqlColumn<String> description;
+        public final SqlColumn<Integer> id;
+        public final SqlColumn<String> description;
 
         public TestTable() {
             super("Test");
diff --git a/src/test/java/org/mybatis/dynamic/sql/util/FragmentCollectorTest.java b/src/test/java/org/mybatis/dynamic/sql/util/FragmentCollectorTest.java
index 8a1d1e7b9..c3f8a5dea 100644
--- a/src/test/java/org/mybatis/dynamic/sql/util/FragmentCollectorTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/util/FragmentCollectorTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/org/mybatis/dynamic/sql/util/PredicatesTest.java b/src/test/java/org/mybatis/dynamic/sql/util/PredicatesTest.java
new file mode 100644
index 000000000..950af088a
--- /dev/null
+++ b/src/test/java/org/mybatis/dynamic/sql/util/PredicatesTest.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.util;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+class PredicatesTest {
+    @Test
+    void testFirstNull() {
+        assertThat(Predicates.bothPresent().test(null, 1)).isFalse();
+    }
+
+    @Test
+    void testSecondNull() {
+        assertThat(Predicates.bothPresent().test(1, null)).isFalse();
+    }
+
+    @Test
+    void testBothNull() {
+        assertThat(Predicates.bothPresent().test(null, null)).isFalse();
+    }
+
+    @Test
+    void testNeitherNull() {
+        assertThat(Predicates.bothPresent().test(1, 1)).isTrue();
+    }
+}
diff --git a/src/test/java/org/mybatis/dynamic/sql/util/SqlProviderAdapterTest.java b/src/test/java/org/mybatis/dynamic/sql/util/SqlProviderAdapterTest.java
index 6cf304698..80c42955b 100644
--- a/src/test/java/org/mybatis/dynamic/sql/util/SqlProviderAdapterTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/util/SqlProviderAdapterTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/org/mybatis/dynamic/sql/util/StringUtilitiesTest.java b/src/test/java/org/mybatis/dynamic/sql/util/StringUtilitiesTest.java
index df6285206..e1d8c137c 100644
--- a/src/test/java/org/mybatis/dynamic/sql/util/StringUtilitiesTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/util/StringUtilitiesTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -38,4 +38,16 @@ void testNumeric() {
         String input = "USER%NAME%3";
         assertThat(StringUtilities.toCamelCase(input)).isEqualTo("userName3");
     }
+
+    @Test
+    void testUpperCaseInteger() {
+        Integer i = StringUtilities.upperCaseIfPossible(3);
+        assertThat(i).isEqualTo(3);
+    }
+
+    @Test
+    void testUpperCaseString() {
+        String i = StringUtilities.upperCaseIfPossible("fred");
+        assertThat(i).isEqualTo("FRED");
+    }
 }
diff --git a/src/test/java/org/mybatis/dynamic/sql/util/UtilitiesTest.java b/src/test/java/org/mybatis/dynamic/sql/util/UtilitiesTest.java
index 3815833c5..230523a64 100644
--- a/src/test/java/org/mybatis/dynamic/sql/util/UtilitiesTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/util/UtilitiesTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/org/mybatis/dynamic/sql/where/WhereModelTest.java b/src/test/java/org/mybatis/dynamic/sql/where/WhereModelTest.java
index 2af9f8044..0b94bda96 100644
--- a/src/test/java/org/mybatis/dynamic/sql/where/WhereModelTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/where/WhereModelTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/org/mybatis/dynamic/sql/where/condition/FilterAndMapTest.java b/src/test/java/org/mybatis/dynamic/sql/where/condition/FilterAndMapTest.java
index 3e35af01b..7ea071e87 100644
--- a/src/test/java/org/mybatis/dynamic/sql/where/condition/FilterAndMapTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/where/condition/FilterAndMapTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,31 +19,26 @@
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 
 import java.util.List;
-import java.util.Objects;
-import java.util.stream.Collectors;
+import java.util.NoSuchElementException;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
 
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
 import org.mybatis.dynamic.sql.SqlBuilder;
 
 class FilterAndMapTest {
     @Test
     void testTypeConversion() {
-        IsEqualTo<Integer> cond = SqlBuilder.isEqualTo("1").map(Integer::parseInt);
+        var cond = SqlBuilder.isEqualTo("1").map(Integer::parseInt);
         assertThat(cond.isEmpty()).isFalse();
         assertThat(cond.value()).isEqualTo(1);
     }
 
-    @Test
-    void testTypeConversionWithNullThrowsException() {
-        IsEqualTo<String> cond = SqlBuilder.isEqualTo((String) null);
-        assertThatExceptionOfType(NumberFormatException.class).isThrownBy(() ->
-            cond.map(Integer::parseInt)
-        );
-    }
-
     @Test
     void testTypeConversionWithNullAndFilterDoesNotThrowException() {
-        IsEqualTo<Integer> cond = SqlBuilder.isEqualTo((String) null).filter(Objects::nonNull).map(Integer::parseInt);
+        var cond = SqlBuilder.isEqualToWhenPresent((String) null).map(Integer::parseInt);
         assertThat(cond.isEmpty()).isTrue();
     }
 
@@ -124,7 +119,7 @@ void testIsEqualMapUnRenderableShouldNotThrowNullPointerException() {
         IsEqualTo<String> cond = SqlBuilder.isEqualTo("Fred").filter(s -> false);
         IsEqualTo<String> mapped = cond.map(String::toUpperCase);
         assertThat(cond.isEmpty()).isTrue();
-        assertThat(cond.value()).isNull();
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value);
         assertThat(cond).isSameAs(mapped);
     }
 
@@ -157,7 +152,7 @@ void testIsNotEqualMapUnRenderableShouldNotThrowNullPointerException() {
         IsNotEqualTo<String> cond = SqlBuilder.isNotEqualTo("Fred").filter(s -> false);
         IsNotEqualTo<String> mapped = cond.map(String::toUpperCase);
         assertThat(cond.isEmpty()).isTrue();
-        assertThat(cond.value()).isNull();
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value);
         assertThat(cond).isSameAs(mapped);
     }
 
@@ -190,7 +185,7 @@ void testIsLessThanMapUnRenderableShouldNotThrowNullPointerException() {
         IsLessThan<String> cond = SqlBuilder.isLessThan("Fred").filter(s -> false);
         IsLessThan<String> mapped = cond.map(String::toUpperCase);
         assertThat(cond.isEmpty()).isTrue();
-        assertThat(cond.value()).isNull();
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value);
         assertThat(cond).isSameAs(mapped);
     }
 
@@ -223,7 +218,7 @@ void testIsLessThanOrEqualMapUnRenderableShouldNotThrowNullPointerException() {
         IsLessThanOrEqualTo<String> cond = SqlBuilder.isLessThanOrEqualTo("Fred").filter(s -> false);
         IsLessThanOrEqualTo<String> mapped = cond.map(String::toUpperCase);
         assertThat(cond.isEmpty()).isTrue();
-        assertThat(cond.value()).isNull();
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value);
         assertThat(cond).isSameAs(mapped);
     }
 
@@ -256,7 +251,7 @@ void testIsGreaterThanMapUnRenderableShouldNotThrowNullPointerException() {
         IsGreaterThan<String> cond = SqlBuilder.isGreaterThan("Fred").filter(s -> false);
         IsGreaterThan<String> mapped = cond.map(String::toUpperCase);
         assertThat(cond.isEmpty()).isTrue();
-        assertThat(cond.value()).isNull();
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value);
         assertThat(cond).isSameAs(mapped);
     }
 
@@ -289,7 +284,7 @@ void testIsGreaterThanOrEqualMapUnRenderableShouldNotThrowNullPointerException()
         IsGreaterThanOrEqualTo<String> cond = SqlBuilder.isGreaterThanOrEqualTo("Fred").filter(s -> false);
         IsGreaterThanOrEqualTo<String> mapped = cond.map(String::toUpperCase);
         assertThat(cond.isEmpty()).isTrue();
-        assertThat(cond.value()).isNull();
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value);
         assertThat(cond).isSameAs(mapped);
     }
 
@@ -322,14 +317,14 @@ void testIsLikeMapUnRenderableShouldNotThrowNullPointerException() {
         IsLike<String> cond = SqlBuilder.isLike("Fred").filter(s -> false);
         IsLike<String> mapped = cond.map(String::toUpperCase);
         assertThat(cond.isEmpty()).isTrue();
-        assertThat(cond.value()).isNull();
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value);
         assertThat(cond).isSameAs(mapped);
     }
 
     @Test
     void testIsLikeCaseInsensitiveRenderableTruePredicateShouldReturnSameObject() {
-        IsLikeCaseInsensitive cond = SqlBuilder.isLikeCaseInsensitive("Fred");
-        IsLikeCaseInsensitive filtered = cond.filter(s -> true);
+        var cond = SqlBuilder.isLikeCaseInsensitive("Fred");
+        var filtered = cond.filter(s -> true);
         assertThat(filtered.value()).isEqualTo("FRED");
         assertThat(filtered.isEmpty()).isFalse();
         assertThat(cond).isSameAs(filtered);
@@ -337,26 +332,26 @@ void testIsLikeCaseInsensitiveRenderableTruePredicateShouldReturnSameObject() {
 
     @Test
     void testIsLikeCaseInsensitiveRenderableFalsePredicate() {
-        IsLikeCaseInsensitive cond = SqlBuilder.isLikeCaseInsensitive("Fred");
-        IsLikeCaseInsensitive filtered = cond.filter(s -> false);
+        var cond = SqlBuilder.isLikeCaseInsensitive("Fred");
+        var filtered = cond.filter(s -> false);
         assertThat(cond.isEmpty()).isFalse();
         assertThat(filtered.isEmpty()).isTrue();
     }
 
     @Test
     void testIsLikeCaseInsensitiveFilterUnRenderableShouldReturnSameObject() {
-        IsLikeCaseInsensitive cond = SqlBuilder.isLikeCaseInsensitive("Fred").filter(s -> false);
-        IsLikeCaseInsensitive filtered = cond.filter(s -> true);
+        var cond = SqlBuilder.isLikeCaseInsensitive("Fred").filter(s -> false);
+        var filtered = cond.filter(s -> true);
         assertThat(filtered.isEmpty()).isTrue();
         assertThat(cond).isSameAs(filtered);
     }
 
     @Test
     void testIsLikeCaseInsensitiveMapUnRenderableShouldNotThrowNullPointerException() {
-        IsLikeCaseInsensitive cond = SqlBuilder.isLikeCaseInsensitive("Fred").filter(s -> false);
-        IsLikeCaseInsensitive mapped = cond.map(String::toUpperCase);
+        IsLikeCaseInsensitive<String> cond = SqlBuilder.isLikeCaseInsensitive("Fred").filter(s -> false);
+        IsLikeCaseInsensitive<String> mapped = cond.map(String::toUpperCase);
         assertThat(cond.isEmpty()).isTrue();
-        assertThat(cond.value()).isNull();
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value);
         assertThat(cond).isSameAs(mapped);
     }
 
@@ -389,14 +384,14 @@ void testIsNotLikeMapUnRenderableShouldNotThrowNullPointerException() {
         IsNotLike<String> cond = SqlBuilder.isNotLike("Fred").filter(s -> false);
         IsNotLike<String> mapped = cond.map(String::toUpperCase);
         assertThat(cond.isEmpty()).isTrue();
-        assertThat(cond.value()).isNull();
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value);
         assertThat(cond).isSameAs(mapped);
     }
 
     @Test
     void testIsNotLikeCaseInsensitiveRenderableTruePredicateShouldReturnSameObject() {
-        IsNotLikeCaseInsensitive cond = SqlBuilder.isNotLikeCaseInsensitive("Fred");
-        IsNotLikeCaseInsensitive filtered = cond.filter(s -> true);
+        var cond = SqlBuilder.isNotLikeCaseInsensitive("Fred");
+        var filtered = cond.filter(s -> true);
         assertThat(filtered.value()).isEqualTo("FRED");
         assertThat(filtered.isEmpty()).isFalse();
         assertThat(cond).isSameAs(filtered);
@@ -404,26 +399,26 @@ void testIsNotLikeCaseInsensitiveRenderableTruePredicateShouldReturnSameObject()
 
     @Test
     void testIsNotLikeCaseInsensitiveRenderableFalsePredicate() {
-        IsNotLikeCaseInsensitive cond = SqlBuilder.isNotLikeCaseInsensitive("Fred");
-        IsNotLikeCaseInsensitive filtered = cond.filter(s -> false);
+        var cond = SqlBuilder.isNotLikeCaseInsensitive("Fred");
+        var filtered = cond.filter(s -> false);
         assertThat(cond.isEmpty()).isFalse();
         assertThat(filtered.isEmpty()).isTrue();
     }
 
     @Test
     void testIsNotLikeCaseInsensitiveFilterUnRenderableShouldReturnSameObject() {
-        IsNotLikeCaseInsensitive cond = SqlBuilder.isNotLikeCaseInsensitive("Fred").filter(s -> false);
-        IsNotLikeCaseInsensitive filtered = cond.filter(s -> true);
+        var cond = SqlBuilder.isNotLikeCaseInsensitive("Fred").filter(s -> false);
+        var filtered = cond.filter(s -> true);
         assertThat(filtered.isEmpty()).isTrue();
         assertThat(cond).isSameAs(filtered);
     }
 
     @Test
     void testIsNotLikeCaseInsensitiveMapUnRenderableShouldNotThrowNullPointerException() {
-        IsNotLikeCaseInsensitive cond = SqlBuilder.isNotLikeCaseInsensitive("Fred").filter(s -> false);
-        IsNotLikeCaseInsensitive mapped = cond.map(String::toUpperCase);
+        var cond = SqlBuilder.isNotLikeCaseInsensitive("Fred").filter(s -> false);
+        var mapped = cond.map(String::toUpperCase);
         assertThat(cond.isEmpty()).isTrue();
-        assertThat(cond.value()).isNull();
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value);
         assertThat(cond).isSameAs(mapped);
     }
 
@@ -432,7 +427,7 @@ void testIsInRenderableMapShouldReturnMappedObject() {
         IsIn<String> cond = SqlBuilder.isIn("Fred", "Wilma");
         assertThat(cond.isEmpty()).isFalse();
         IsIn<String> mapped = cond.map(String::toUpperCase);
-        List<String> mappedValues = mapped.values().collect(Collectors.toList());
+        List<String> mappedValues = mapped.values().toList();
         assertThat(mappedValues).containsExactly("FRED", "WILMA");
     }
 
@@ -441,31 +436,31 @@ void testIsNotInRenderableMapShouldReturnMappedObject() {
         IsNotIn<String> cond = SqlBuilder.isNotIn("Fred", "Wilma");
         assertThat(cond.isEmpty()).isFalse();
         IsNotIn<String> mapped = cond.map(String::toUpperCase);
-        List<String> mappedValues = mapped.values().collect(Collectors.toList());
+        List<String> mappedValues = mapped.values().toList();
         assertThat(mappedValues).containsExactly("FRED", "WILMA");
     }
 
     @Test
     void testIsNotInCaseInsensitiveRenderableMapShouldReturnMappedObject() {
-        IsNotInCaseInsensitive cond = SqlBuilder.isNotInCaseInsensitive("Fred  ", "Wilma  ");
-        List<String> values = cond.values().collect(Collectors.toList());
+        var cond = SqlBuilder.isNotInCaseInsensitive("Fred  ", "Wilma  ");
+        var values = cond.values().toList();
         assertThat(values).containsExactly("FRED  ", "WILMA  ");
         assertThat(cond.isEmpty()).isFalse();
 
-        IsNotInCaseInsensitive mapped = cond.map(String::trim);
-        List<String> mappedValues = mapped.values().collect(Collectors.toList());
+        var mapped = cond.map(String::trim);
+        var mappedValues = mapped.values().toList();
         assertThat(mappedValues).containsExactly("FRED", "WILMA");
     }
 
     @Test
     void testIsInCaseInsensitiveRenderableMapShouldReturnMappedObject() {
-        IsInCaseInsensitive cond = SqlBuilder.isInCaseInsensitive("Fred  ", "Wilma  ");
-        List<String> values = cond.values().collect(Collectors.toList());
+        var cond = SqlBuilder.isInCaseInsensitive("Fred  ", "Wilma  ");
+        var values = cond.values().toList();
         assertThat(values).containsExactly("FRED  ", "WILMA  ");
         assertThat(cond.isEmpty()).isFalse();
 
-        IsInCaseInsensitive mapped = cond.map(String::trim);
-        List<String> mappedValues = mapped.values().collect(Collectors.toList());
+        var mapped = cond.map(String::trim);
+        var mappedValues = mapped.values().toList();
         assertThat(mappedValues).containsExactly("FRED", "WILMA");
     }
 
@@ -479,17 +474,17 @@ void testBetweenUnRenderableFilterShouldReturnSameObject() {
 
     @Test
     void testBetweenUnRenderableFirstNullFilterShouldReturnSameObject() {
-        IsBetween<Integer> cond = SqlBuilder.isBetween((Integer) null).and(4).filter(Objects::nonNull);
+        IsBetweenWhenPresent<Integer> cond = SqlBuilder.isBetweenWhenPresent((Integer) null).and(4);
         assertThat(cond.isEmpty()).isTrue();
-        IsBetween<Integer> filtered = cond.filter(v -> true);
+        IsBetweenWhenPresent<Integer> filtered = cond.filter(v -> true);
         assertThat(cond).isSameAs(filtered);
     }
 
     @Test
     void testBetweenUnRenderableSecondNullFilterShouldReturnSameObject() {
-        IsBetween<Integer> cond = SqlBuilder.isBetween(3).and((Integer) null).filter(Objects::nonNull);
+        IsBetweenWhenPresent<Integer> cond = SqlBuilder.isBetweenWhenPresent(3).and((Integer) null);
         assertThat(cond.isEmpty()).isTrue();
-        IsBetween<Integer> filtered = cond.filter(v -> true);
+        IsBetweenWhenPresent<Integer> filtered = cond.filter(v -> true);
         assertThat(cond).isSameAs(filtered);
     }
 
@@ -501,6 +496,46 @@ void testBetweenMapWithSingleMapper() {
         assertThat(cond.value2()).isEqualTo(4);
     }
 
+    @Test
+    void testBetweenWhenPresentFilterWithBiPredicate() {
+        IsBetweenWhenPresent<Integer> cond = SqlBuilder.isBetweenWhenPresent("3").and("4")
+                .map(Integer::parseInt)
+                .filter((v1, v2) -> true);
+        assertThat(cond.isEmpty()).isFalse();
+        assertThat(cond.value1()).isEqualTo(3);
+        assertThat(cond.value2()).isEqualTo(4);
+    }
+
+    @Test
+    void testNotBetweenWhenPresentFilterWithBiPredicate() {
+        IsNotBetweenWhenPresent<Integer> cond = SqlBuilder.isNotBetweenWhenPresent("3").and("4")
+                .map(Integer::parseInt)
+                .filter((v1, v2) -> true);
+        assertThat(cond.isEmpty()).isFalse();
+        assertThat(cond.value1()).isEqualTo(3);
+        assertThat(cond.value2()).isEqualTo(4);
+    }
+
+    @ParameterizedTest
+    @MethodSource("testBetweenFilterVariations")
+    void testBetweenFilterVariations(FilterVariation variation) {
+        IsBetween<Integer> cond = SqlBuilder.isBetween("4").and("6")
+                .map(Integer::parseInt)
+                .filter(variation.predicate);
+        assertThat(cond.isEmpty()).isEqualTo(variation.empty);
+    }
+
+    private record FilterVariation(Predicate<Integer> predicate, boolean empty) {}
+
+    private static Stream<FilterVariation> testBetweenFilterVariations() {
+        return Stream.of(
+                new FilterVariation(v -> v == 4, true),
+                new FilterVariation(v -> v == 6, true),
+                new FilterVariation(v -> v == 1, true),
+                new FilterVariation(v -> v % 2 == 0, false)
+        );
+    }
+
     @Test
     void testNotBetweenUnRenderableFilterShouldReturnSameObject() {
         IsNotBetween<Integer> cond = SqlBuilder.isNotBetween(3).and(4).filter((i1, i2) -> false);
@@ -511,17 +546,17 @@ void testNotBetweenUnRenderableFilterShouldReturnSameObject() {
 
     @Test
     void testNotBetweenUnRenderableFirstNullFilterShouldReturnSameObject() {
-        IsNotBetween<Integer> cond = SqlBuilder.isNotBetween((Integer) null).and(4).filter(Objects::nonNull);
+        IsNotBetweenWhenPresent<Integer> cond = SqlBuilder.isNotBetweenWhenPresent((Integer) null).and(4);
         assertThat(cond.isEmpty()).isTrue();
-        IsNotBetween<Integer> filtered = cond.filter(v -> true);
+        IsNotBetweenWhenPresent<Integer> filtered = cond.filter(v -> true);
         assertThat(cond).isSameAs(filtered);
     }
 
     @Test
     void testNotBetweenUnRenderableSecondNullFilterShouldReturnSameObject() {
-        IsNotBetween<Integer> cond = SqlBuilder.isNotBetween(3).and((Integer) null).filter(Objects::nonNull);
+        IsNotBetweenWhenPresent<Integer> cond = SqlBuilder.isNotBetweenWhenPresent(3).and((Integer) null);
         assertThat(cond.isEmpty()).isTrue();
-        IsNotBetween<Integer> filtered = cond.filter(v -> true);
+        IsNotBetweenWhenPresent<Integer> filtered = cond.filter(v -> true);
         assertThat(cond).isSameAs(filtered);
     }
 
@@ -533,4 +568,44 @@ void testNotBetweenMapWithSingleMapper() {
         assertThat(cond.value2()).isEqualTo(4);
     }
 
+    @Test
+    void testBetweenFilterToEmpty() {
+        var cond = SqlBuilder.isBetween("3").and("4").map(Integer::parseInt)
+                .filter((i1, i2) -> false);
+        assertThat(cond.isEmpty()).isTrue();
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value1);
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value2);
+    }
+
+    @Test
+    void testNotBetweenFilterToEmpty() {
+        var cond = SqlBuilder.isNotBetween("3").and("4").map(Integer::parseInt)
+                .filter((i1, i2) -> false);
+        assertThat(cond.isEmpty()).isTrue();
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value1);
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value2);
+    }
+
+    @Test
+    void testMappingAnEmptyListCondition() {
+        var cond = SqlBuilder.isNotIn("Fred", "Wilma");
+        var filtered = cond.filter(s -> false);
+        var mapped = filtered.map(s -> s);
+        assertThat(mapped.isEmpty()).isTrue();
+        assertThat(filtered).isSameAs(mapped);
+    }
+
+    @Test
+    void testIsInCaseInsensitiveWhenPresentMapCaseInsensitive() {
+        var cond = SqlBuilder.isInCaseInsensitiveWhenPresent("Fred", "Wilma");
+        var mapped = cond.map(s -> s + " Flintstone");
+        assertThat(mapped.values().toList()).containsExactly("FRED FLINTSTONE", "WILMA FLINTSTONE");
+    }
+
+    @Test
+    void testIsNotInCaseInsensitiveWhenPresentMapCaseInsensitive() {
+        var cond = SqlBuilder.isNotInCaseInsensitiveWhenPresent("Fred", "Wilma");
+        var mapped = cond.map(s -> s + " Flintstone");
+        assertThat(mapped.values().toList()).containsExactly("FRED FLINTSTONE", "WILMA FLINTSTONE");
+    }
 }
diff --git a/src/test/java/org/mybatis/dynamic/sql/where/condition/NullContractTest.java b/src/test/java/org/mybatis/dynamic/sql/where/condition/NullContractTest.java
new file mode 100644
index 000000000..cd55d9e52
--- /dev/null
+++ b/src/test/java/org/mybatis/dynamic/sql/where/condition/NullContractTest.java
@@ -0,0 +1,546 @@
+/*
+ *    Copyright 2016-2025 the original author or authors.
+ *
+ *    Licensed under the Apache License, Version 2.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 static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+import java.util.NoSuchElementException;
+
+import org.junit.jupiter.api.Test;
+import org.mybatis.dynamic.sql.SqlBuilder;
+
+/**
+ * This set of tests verifies that the library handles null values in conditions as expected.
+ *
+ * <p>In version 2.0, we adopted JSpecify which brought several issues to light.
+ * In general, the library does not support passing null values into methods unless the method
+ * is a "whenPresent" method. However, from the beginning the library has handled null values in conditions
+ * by placing a null into the generated parameter map. We consider this a misuse of the library, but we are
+ * keeping that behavior for compatibility.
+ *
+ * <p>In a future version, we will stop supporting this misuse.
+ *
+ * <p>This set of tests should be the only tests in the library that verify this behavior. All other tests
+ * should use the library properly.
+ */
+class NullContractTest {
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsBetween() {
+        IsBetween<Integer> nullCond = SqlBuilder.isBetween((Integer) null).and((Integer) null);
+        assertThat(nullCond.isEmpty()).isFalse();
+
+        IsBetween<Integer> cond = SqlBuilder.isBetween(1).and(10);
+        IsBetween<Integer> filtered = cond.filter(i -> i >= 1);
+        IsBetween<Integer> mapped = filtered.map(i -> null);
+        assertThat(mapped.isEmpty()).isFalse();
+
+        mapped = filtered.map(v1 -> null, v2 -> null);
+        assertThat(mapped.isEmpty()).isFalse();
+        assertThat(mapped.value1()).isNull();
+        assertThat(mapped.value2()).isNull();
+    }
+
+    @Test
+    void testIsBetweenWhenPresent() {
+        IsBetweenWhenPresent<Integer> nullCond = SqlBuilder.isBetweenWhenPresent((Integer) null).and((Integer) null);
+        assertThat(nullCond.isEmpty()).isTrue();
+
+        IsBetweenWhenPresent<Integer> cond = SqlBuilder.isBetweenWhenPresent(1).and(10);
+        IsBetweenWhenPresent<Integer> filtered = cond.filter(i -> i == 1);
+        IsBetweenWhenPresent<Integer> mapped = filtered.map(i -> null);
+        assertThat(mapped.isEmpty()).isTrue();
+
+        mapped = filtered.map(v1 -> null, v2 -> null);
+        assertThat(mapped.isEmpty()).isTrue();
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(mapped::value1);
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(mapped::value2);
+    }
+
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsEqualTo() {
+        IsEqualTo<Integer> nullCond = SqlBuilder.isEqualTo((Integer) null); // should be an IDE warning
+        assertThat(nullCond.isEmpty()).isFalse();
+
+        IsEqualTo<Integer> cond = SqlBuilder.isEqualTo(1);
+        IsEqualTo<Integer> filtered = cond.filter(i -> i == 1);
+        IsEqualTo<Integer> mapped = filtered.map(i -> null);  // should be an IDE warning
+        assertThat(mapped.isEmpty()).isFalse();
+        assertThat(mapped.value()).isNull();
+    }
+
+    @Test
+    void testIsEqualToWhenPresent() {
+        IsEqualToWhenPresent<Integer> nullCond = SqlBuilder.isEqualToWhenPresent((Integer) null);
+        assertThat(nullCond.isEmpty()).isTrue();
+
+        IsEqualToWhenPresent<Integer> cond = SqlBuilder.isEqualToWhenPresent(1);
+        IsEqualToWhenPresent<Integer> filtered = cond.filter(i -> i == 1);
+        IsEqualToWhenPresent<Integer> mapped = filtered.map(i -> null);
+        assertThat(mapped.isEmpty()).isTrue();
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(mapped::value);
+    }
+
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsGreaterThan() {
+        IsGreaterThan<Integer> nullCond = SqlBuilder.isGreaterThan((Integer) null); // should be an IDE warning
+        assertThat(nullCond.isEmpty()).isFalse();
+
+        IsGreaterThan<Integer> cond = SqlBuilder.isGreaterThan(1);
+        IsGreaterThan<Integer> filtered = cond.filter(i -> i == 1);
+        IsGreaterThan<Integer> mapped = filtered.map(i -> null);  // should be an IDE warning
+        assertThat(mapped.isEmpty()).isFalse();
+        assertThat(mapped.value()).isNull();
+    }
+
+    @Test
+    void testIsGreaterThanWhenPresent() {
+        IsGreaterThanWhenPresent<Integer> nullCond = SqlBuilder.isGreaterThanWhenPresent((Integer) null);
+        assertThat(nullCond.isEmpty()).isTrue();
+
+        IsGreaterThanWhenPresent<Integer> cond = SqlBuilder.isGreaterThanWhenPresent(1);
+        IsGreaterThanWhenPresent<Integer> filtered = cond.filter(i -> i == 1);
+        IsGreaterThanWhenPresent<Integer> mapped = filtered.map(i -> null);
+        assertThat(mapped.isEmpty()).isTrue();
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(mapped::value);
+    }
+
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsGreaterThanOrEqualTo() {
+        IsGreaterThanOrEqualTo<Integer> nullCond = SqlBuilder.isGreaterThanOrEqualTo((Integer) null); // should be an IDE warning
+        assertThat(nullCond.isEmpty()).isFalse();
+
+        IsGreaterThanOrEqualTo<Integer> cond = SqlBuilder.isGreaterThanOrEqualTo(1);
+        IsGreaterThanOrEqualTo<Integer> filtered = cond.filter(i -> i == 1);
+        IsGreaterThanOrEqualTo<Integer> mapped = filtered.map(i -> null);  // should be an IDE warning
+        assertThat(mapped.isEmpty()).isFalse();
+        assertThat(mapped.value()).isNull();
+    }
+
+    @Test
+    void testIsGreaterThanOrEqualToWhenPresent() {
+        IsGreaterThanOrEqualToWhenPresent<Integer> nullCond = SqlBuilder.isGreaterThanOrEqualToWhenPresent((Integer) null);
+        assertThat(nullCond.isEmpty()).isTrue();
+
+        IsGreaterThanOrEqualToWhenPresent<Integer> cond = SqlBuilder.isGreaterThanOrEqualToWhenPresent(1);
+        IsGreaterThanOrEqualToWhenPresent<Integer> filtered = cond.filter(i -> i == 1);
+        IsGreaterThanOrEqualToWhenPresent<Integer> mapped = filtered.map(i -> null);
+        assertThat(mapped.isEmpty()).isTrue();
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(mapped::value);
+    }
+
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsLessThan() {
+        IsLessThan<Integer> nullCond = SqlBuilder.isLessThan((Integer) null); // should be an IDE warning
+        assertThat(nullCond.isEmpty()).isFalse();
+
+        IsLessThan<Integer> cond = SqlBuilder.isLessThan(1);
+        IsLessThan<Integer> filtered = cond.filter(i -> i == 1);
+        IsLessThan<Integer> mapped = filtered.map(i -> null);  // should be an IDE warning
+        assertThat(mapped.isEmpty()).isFalse();
+        assertThat(mapped.value()).isNull();
+    }
+
+    @Test
+    void testIsLessThanWhenPresent() {
+        IsLessThanWhenPresent<Integer> nullCond = SqlBuilder.isLessThanWhenPresent((Integer) null);
+        assertThat(nullCond.isEmpty()).isTrue();
+
+        IsLessThanWhenPresent<Integer> cond = SqlBuilder.isLessThanWhenPresent(1);
+        IsLessThanWhenPresent<Integer> filtered = cond.filter(i -> i == 1);
+        IsLessThanWhenPresent<Integer> mapped = filtered.map(i -> null);
+        assertThat(mapped.isEmpty()).isTrue();
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(mapped::value);
+    }
+
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsLessThanOrEqualTo() {
+        IsLessThanOrEqualTo<Integer> nullCond = SqlBuilder.isLessThanOrEqualTo((Integer) null); // should be an IDE warning
+        assertThat(nullCond.isEmpty()).isFalse();
+
+        IsLessThanOrEqualTo<Integer> cond = SqlBuilder.isLessThanOrEqualTo(1);
+        IsLessThanOrEqualTo<Integer> filtered = cond.filter(i -> i == 1);
+        IsLessThanOrEqualTo<Integer> mapped = filtered.map(i -> null);  // should be an IDE warning
+        assertThat(mapped.isEmpty()).isFalse();
+        assertThat(mapped.value()).isNull();
+    }
+
+    @Test
+    void testIsLessThanOrEqualToWhenPresent() {
+        IsLessThanOrEqualToWhenPresent<Integer> nullCond = SqlBuilder.isLessThanOrEqualToWhenPresent((Integer) null);
+        assertThat(nullCond.isEmpty()).isTrue();
+
+        IsLessThanOrEqualToWhenPresent<Integer> cond = SqlBuilder.isLessThanOrEqualToWhenPresent(1);
+        IsLessThanOrEqualToWhenPresent<Integer> filtered = cond.filter(i -> i == 1);
+        IsLessThanOrEqualToWhenPresent<Integer> mapped = filtered.map(i -> null);
+        assertThat(mapped.isEmpty()).isTrue();
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(mapped::value);
+    }
+
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsNotBetween() {
+        IsNotBetween<Integer> nullCond = SqlBuilder.isNotBetween((Integer) null).and((Integer) null);
+        assertThat(nullCond.isEmpty()).isFalse();
+
+        IsNotBetween<Integer> cond = SqlBuilder.isNotBetween(1).and(10);
+        IsNotBetween<Integer> filtered = cond.filter(i -> i >= 1);
+        IsNotBetween<Integer> mapped = filtered.map(i -> null);
+        assertThat(mapped.isEmpty()).isFalse();
+
+        mapped = filtered.map(v1 -> null, v2 -> null);
+        assertThat(mapped.isEmpty()).isFalse();
+        assertThat(mapped.value1()).isNull();
+        assertThat(mapped.value2()).isNull();
+    }
+
+    @Test
+    void testIsNotBetweenWhenPresent() {
+        IsNotBetweenWhenPresent<Integer> nullCond = SqlBuilder.isNotBetweenWhenPresent((Integer) null).and((Integer) null);
+        assertThat(nullCond.isEmpty()).isTrue();
+
+        IsNotBetweenWhenPresent<Integer> cond = SqlBuilder.isNotBetweenWhenPresent(1).and(10);
+        IsNotBetweenWhenPresent<Integer> filtered = cond.filter(i -> i == 1);
+        IsNotBetweenWhenPresent<Integer> mapped = filtered.map(i -> null);
+        assertThat(mapped.isEmpty()).isTrue();
+
+        mapped = filtered.map(v1 -> null, v2 -> null);
+        assertThat(mapped.isEmpty()).isTrue();
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(mapped::value1);
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(mapped::value2);
+    }
+
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsNotEqualTo() {
+        IsNotEqualTo<Integer> nullCond = SqlBuilder.isNotEqualTo((Integer) null); // should be an IDE warning
+        assertThat(nullCond.isEmpty()).isFalse();
+
+        IsNotEqualTo<Integer> cond = SqlBuilder.isNotEqualTo(1);
+        IsNotEqualTo<Integer> filtered = cond.filter(i -> i == 1);
+        IsNotEqualTo<Integer> mapped = filtered.map(i -> null); // should be an IDE warning
+        assertThat(mapped.isEmpty()).isFalse();
+        assertThat(mapped.value()).isNull();
+    }
+
+    @Test
+    void testIsNotEqualToWhenPresent() {
+        IsNotEqualToWhenPresent<Integer> nullCond = SqlBuilder.isNotEqualToWhenPresent((Integer) null);
+        assertThat(nullCond.isEmpty()).isTrue();
+
+        IsNotEqualToWhenPresent<Integer> cond = SqlBuilder.isNotEqualToWhenPresent(1);
+        IsNotEqualToWhenPresent<Integer> filtered = cond.filter(i -> i == 1);
+        IsNotEqualToWhenPresent<Integer> mapped = filtered.map(i -> null);
+        assertThat(mapped.isEmpty()).isTrue();
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(mapped::value);
+    }
+
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsLike() {
+        IsLike<String> nullCond = SqlBuilder.isLike((String) null); // should be an IDE warning
+        assertThat(nullCond.isEmpty()).isFalse();
+
+        IsLike<String> cond = SqlBuilder.isLike("fred");
+        IsLike<String> filtered = cond.filter(i -> i.equals("fred"));
+        IsLike<String> mapped = filtered.map(i -> null); // should be an IDE warning
+        assertThat(mapped.isEmpty()).isFalse();
+        assertThat(mapped.value()).isNull();
+    }
+
+    @Test
+    void testIsLikeWhenPresent() {
+        IsLikeWhenPresent<String> nullCond = SqlBuilder.isLikeWhenPresent((String) null);
+        assertThat(nullCond.isEmpty()).isTrue();
+
+        IsLikeWhenPresent<String> cond = SqlBuilder.isLikeWhenPresent("fred");
+        IsLikeWhenPresent<String> filtered = cond.filter(i -> i.equals("fred"));
+        IsLikeWhenPresent<String> mapped = filtered.map(i -> null);
+        assertThat(mapped.isEmpty()).isTrue();
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(mapped::value);
+    }
+
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsLikeCaseInsensitive() {
+        IsLikeCaseInsensitive<String> nullCond = SqlBuilder.isLikeCaseInsensitive((String) null); // should be an IDE warning
+        assertThat(nullCond.isEmpty()).isFalse();
+
+        IsLikeCaseInsensitive<String> cond = SqlBuilder.isLikeCaseInsensitive("fred");
+        IsLikeCaseInsensitive<String> filtered = cond.filter(i -> i.equals("FRED"));
+        IsLikeCaseInsensitive<String> mapped = filtered.map(i -> null); // should be an IDE warning
+        assertThat(mapped.isEmpty()).isFalse();
+        assertThat(mapped.value()).isNull();
+    }
+
+    @Test
+    void testIsLikeCaseInsensitiveWhenPresent() {
+        IsLikeCaseInsensitiveWhenPresent<String> nullCond = SqlBuilder.isLikeCaseInsensitiveWhenPresent((String) null);
+        assertThat(nullCond.isEmpty()).isTrue();
+
+        IsLikeCaseInsensitiveWhenPresent<String> cond = SqlBuilder.isLikeCaseInsensitiveWhenPresent("fred");
+        IsLikeCaseInsensitiveWhenPresent<String> filtered = cond.filter(i -> i.equals("fred"));
+        IsLikeCaseInsensitiveWhenPresent<String> mapped = filtered.map(i -> null);
+        assertThat(mapped.isEmpty()).isTrue();
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(mapped::value);
+    }
+
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsNotLike() {
+        IsNotLike<String> nullCond = SqlBuilder.isNotLike((String) null); // should be an IDE warning
+        assertThat(nullCond.isEmpty()).isFalse();
+
+        IsNotLike<String> cond = SqlBuilder.isNotLike("fred");
+        IsNotLike<String> filtered = cond.filter(i -> i.equals("fred"));
+        IsNotLike<String> mapped = filtered.map(i -> null); // should be an IDE warning
+        assertThat(mapped.isEmpty()).isFalse();
+        assertThat(mapped.value()).isNull();
+    }
+
+    @Test
+    void testIsNotLikeWhenPresent() {
+        IsNotLikeWhenPresent<String> nullCond = SqlBuilder.isNotLikeWhenPresent((String) null);
+        assertThat(nullCond.isEmpty()).isTrue();
+
+        IsNotLikeWhenPresent<String> cond = SqlBuilder.isNotLikeWhenPresent("fred");
+        IsNotLikeWhenPresent<String> filtered = cond.filter(i -> i.equals("fred"));
+        IsNotLikeWhenPresent<String> mapped = filtered.map(i -> null);
+        assertThat(mapped.isEmpty()).isTrue();
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(mapped::value);
+    }
+
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsNotLikeCaseInsensitive() {
+        IsNotLikeCaseInsensitive<String> nullCond = SqlBuilder.isNotLikeCaseInsensitive((String) null); // should be an IDE warning
+        assertThat(nullCond.isEmpty()).isFalse();
+
+        IsNotLikeCaseInsensitive<String> cond = SqlBuilder.isNotLikeCaseInsensitive("fred");
+        IsNotLikeCaseInsensitive<String> filtered = cond.filter(i -> i.equals("FRED"));
+        IsNotLikeCaseInsensitive<String> mapped = filtered.map(i -> null); // should be an IDE warning
+        assertThat(mapped.isEmpty()).isFalse();
+        assertThat(mapped.value()).isNull();
+    }
+
+    @Test
+    void testIsNotLikeCaseInsensitiveWhenPresent() {
+        IsNotLikeCaseInsensitiveWhenPresent<String> nullCond = SqlBuilder.isNotLikeCaseInsensitiveWhenPresent((String) null);
+        assertThat(nullCond.isEmpty()).isTrue();
+
+        IsNotLikeCaseInsensitiveWhenPresent<String> cond = SqlBuilder.isNotLikeCaseInsensitiveWhenPresent("fred");
+        IsNotLikeCaseInsensitiveWhenPresent<String> filtered = cond.filter(i -> i.equals("FRED"));
+        IsNotLikeCaseInsensitiveWhenPresent<String> mapped = filtered.map(i -> null);
+        assertThat(mapped.isEmpty()).isTrue();
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(mapped::value);
+    }
+
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsIn() {
+        IsIn<Integer> nullCond = SqlBuilder.isIn((Integer) null); // should be an IDE warning
+        assertThat(nullCond.isEmpty()).isFalse();
+
+        IsIn<Integer> cond = SqlBuilder.isIn(1);
+        IsIn<Integer> filtered = cond.filter(i -> i == 1);
+        IsIn<Integer> mapped = filtered.map(i -> null); // should be an IDE warning
+        assertThat(mapped.isEmpty()).isFalse();
+        assertThat(mapped.values().toList()).containsExactly((Integer) null);
+    }
+
+    @Test
+    void testIsInWhenPresent() {
+        IsInWhenPresent<Integer> nullCond = SqlBuilder.isInWhenPresent((Integer) null);
+        assertThat(nullCond.isEmpty()).isTrue();
+
+        IsInWhenPresent<Integer> cond = SqlBuilder.isInWhenPresent(1);
+        IsInWhenPresent<Integer> filtered = cond.filter(i -> i == 1);
+        IsInWhenPresent<Integer> mapped = filtered.map(i -> null);
+        assertThat(mapped.isEmpty()).isTrue();
+        assertThat(mapped.values().toList()).isEmpty();
+    }
+
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsInCaseInsensitive() {
+        IsInCaseInsensitive<String> nullCond = SqlBuilder.isInCaseInsensitive((String) null); // should be an IDE warning
+        assertThat(nullCond.isEmpty()).isFalse();
+
+        IsInCaseInsensitive<String> cond = SqlBuilder.isInCaseInsensitive("fred");
+        IsInCaseInsensitive<String> filtered = cond.filter(i -> i.equals("FRED"));
+        IsInCaseInsensitive<String> mapped = filtered.map(i -> null); // should be an IDE warning
+        assertThat(mapped.isEmpty()).isFalse();
+        assertThat(mapped.values().toList()).containsExactly((String) null);
+    }
+
+    @Test
+    void testIsInCaseInsensitiveWhenPresent() {
+        IsInCaseInsensitiveWhenPresent<String> nullCond = SqlBuilder.isInCaseInsensitiveWhenPresent((String) null);
+        assertThat(nullCond.isEmpty()).isTrue();
+
+        IsInCaseInsensitiveWhenPresent<String> cond = SqlBuilder.isInCaseInsensitiveWhenPresent("fred");
+        IsInCaseInsensitiveWhenPresent<String> filtered = cond.filter(i -> i.equals("FRED"));
+        IsInCaseInsensitiveWhenPresent<String> mapped = filtered.map(i -> null);
+        assertThat(mapped.isEmpty()).isTrue();
+        assertThat(mapped.values().toList()).isEmpty();
+    }
+
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsNotIn() {
+        IsNotIn<Integer> nullCond = SqlBuilder.isNotIn((Integer) null); // should be an IDE warning
+        assertThat(nullCond.isEmpty()).isFalse();
+
+        IsNotIn<Integer> cond = SqlBuilder.isNotIn(1);
+        IsNotIn<Integer> filtered = cond.filter(i -> i == 1);
+        IsNotIn<Integer> mapped = filtered.map(i -> null); // should be an IDE warning
+        assertThat(mapped.isEmpty()).isFalse();
+        assertThat(mapped.values().toList()).containsExactly((Integer) null);
+    }
+
+    @Test
+    void testIsNotInWhenPresent() {
+        IsNotInWhenPresent<Integer> nullCond = SqlBuilder.isNotInWhenPresent((Integer) null);
+        assertThat(nullCond.isEmpty()).isTrue();
+
+        IsNotInWhenPresent<Integer> cond = SqlBuilder.isNotInWhenPresent(1);
+        IsNotInWhenPresent<Integer> filtered = cond.filter(i -> i == 1);
+        IsNotInWhenPresent<Integer> mapped = filtered.map(i -> null);
+        assertThat(mapped.isEmpty()).isTrue();
+        assertThat(mapped.values().toList()).isEmpty();
+    }
+
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsNotInCaseInsensitive() {
+        IsNotInCaseInsensitive<String> nullCond = SqlBuilder.isNotInCaseInsensitive((String) null); // should be an IDE warning
+        assertThat(nullCond.isEmpty()).isFalse();
+
+        IsNotInCaseInsensitive<String> cond = SqlBuilder.isNotInCaseInsensitive("fred");
+        IsNotInCaseInsensitive<String> filtered = cond.filter(i -> i.equals("FRED"));
+        IsNotInCaseInsensitive<String> mapped = filtered.map(i -> null); // should be an IDE warning
+        assertThat(mapped.isEmpty()).isFalse();
+        assertThat(mapped.values().toList()).containsExactly((String) null);
+    }
+
+    @Test
+    void testIsNotInCaseInsensitiveWhenPresent() {
+        IsNotInCaseInsensitiveWhenPresent<String> nullCond = SqlBuilder.isNotInCaseInsensitiveWhenPresent((String) null);
+        assertThat(nullCond.isEmpty()).isTrue();
+
+        IsNotInCaseInsensitiveWhenPresent<String> cond = SqlBuilder.isNotInCaseInsensitiveWhenPresent("fred");
+        IsNotInCaseInsensitiveWhenPresent<String> filtered = cond.filter(i -> i.equals("FRED"));
+        IsNotInCaseInsensitiveWhenPresent<String> mapped = filtered.map(i -> null);
+        assertThat(mapped.isEmpty()).isTrue();
+        assertThat(mapped.values().toList()).isEmpty();
+    }
+
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsBetweenNull() {
+        IsBetween<Integer> cond = SqlBuilder.isBetween(() -> (Integer) null).and(() -> null);
+        assertThat(cond.value1()).isNull();
+        assertThat(cond.value2()).isNull();
+        assertThat(cond.isEmpty()).isFalse();
+    }
+
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsNotBetweenNull() {
+        IsNotBetween<Integer> cond = SqlBuilder.isNotBetween(() -> (Integer) null).and(() -> null);
+        assertThat(cond.value1()).isNull();
+        assertThat(cond.value2()).isNull();
+        assertThat(cond.isEmpty()).isFalse();
+    }
+
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsNotEqualToNull() {
+        IsNotEqualTo<Integer> cond = SqlBuilder.isNotEqualTo(() -> (Integer) null);
+        assertThat(cond.value()).isNull();
+        assertThat(cond.isEmpty()).isFalse();
+    }
+
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsGreaterThanNull() {
+        IsGreaterThan<Integer> cond = SqlBuilder.isGreaterThan(() -> (Integer) null);
+        assertThat(cond.value()).isNull();
+        assertThat(cond.isEmpty()).isFalse();
+    }
+
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsGreaterThanOrEqualToNull() {
+        IsGreaterThanOrEqualTo<Integer> cond = SqlBuilder.isGreaterThanOrEqualTo(() -> (Integer) null);
+        assertThat(cond.value()).isNull();
+        assertThat(cond.isEmpty()).isFalse();
+    }
+
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsLessThanNull() {
+        IsLessThan<Integer> cond = SqlBuilder.isLessThan(() -> (Integer) null);
+        assertThat(cond.value()).isNull();
+        assertThat(cond.isEmpty()).isFalse();
+    }
+
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsLessThanOrEqualToNull() {
+        IsLessThanOrEqualTo<Integer> cond = SqlBuilder.isLessThanOrEqualTo(() -> (Integer) null);
+        assertThat(cond.value()).isNull();
+        assertThat(cond.isEmpty()).isFalse();
+    }
+
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsLikeNull() {
+        IsLike<String> cond = SqlBuilder.isLike(() -> null);
+        assertThat(cond.value()).isNull();
+        assertThat(cond.isEmpty()).isFalse();
+    }
+
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsLikeCaseInsensitiveNull() {
+        IsLikeCaseInsensitive<String> cond = SqlBuilder.isLikeCaseInsensitive(() -> null);
+        assertThat(cond.value()).isNull();
+        assertThat(cond.isEmpty()).isFalse();
+    }
+
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsNotLikeNull() {
+        IsNotLike<String> cond = SqlBuilder.isNotLike(() -> null);
+        assertThat(cond.value()).isNull();
+        assertThat(cond.isEmpty()).isFalse();
+    }
+
+    @SuppressWarnings("DataFlowIssue") // we are deliberately passing nulls into non-null methods for testing
+    @Test
+    void testIsNotLikeCaseInsensitiveNull() {
+        IsNotLikeCaseInsensitive<String> cond = SqlBuilder.isNotLikeCaseInsensitive(() -> null);
+        assertThat(cond.value()).isNull();
+        assertThat(cond.isEmpty()).isFalse();
+    }
+}
diff --git a/src/test/java/org/mybatis/dynamic/sql/where/condition/SupplierTest.java b/src/test/java/org/mybatis/dynamic/sql/where/condition/SupplierTest.java
index ed8cf7c5b..113c60321 100644
--- a/src/test/java/org/mybatis/dynamic/sql/where/condition/SupplierTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/where/condition/SupplierTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -16,10 +16,13 @@
 package org.mybatis.dynamic.sql.where.condition;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 
 import org.junit.jupiter.api.Test;
 import org.mybatis.dynamic.sql.SqlBuilder;
 
+import java.util.NoSuchElementException;
+
 // series of tests to check that the Supplier based DSL methods function correctly
 
 class SupplierTest {
@@ -31,17 +34,9 @@ void testIsBetween() {
         assertThat(cond.isEmpty()).isFalse();
     }
 
-    @Test
-    void testIsBetweenNull() {
-        IsBetween<Integer> cond = SqlBuilder.isBetween(() -> (Integer) null).and(() -> null);
-        assertThat(cond.value1()).isNull();
-        assertThat(cond.value2()).isNull();
-        assertThat(cond.isEmpty()).isFalse();
-    }
-
     @Test
     void testIsBetweenWhenPresent() {
-        IsBetween<Integer> cond = SqlBuilder.isBetweenWhenPresent(() -> 3).and(() -> 4);
+        IsBetweenWhenPresent<Integer> cond = SqlBuilder.isBetweenWhenPresent(() -> 3).and(() -> 4);
         assertThat(cond.value1()).isEqualTo(3);
         assertThat(cond.value2()).isEqualTo(4);
         assertThat(cond.isEmpty()).isFalse();
@@ -49,9 +44,9 @@ void testIsBetweenWhenPresent() {
 
     @Test
     void testIsBetweenWhenPresentNull() {
-        IsBetween<Integer> cond = SqlBuilder.isBetweenWhenPresent(() -> (Integer) null).and(() -> null);
-        assertThat(cond.value1()).isNull();
-        assertThat(cond.value2()).isNull();
+        IsBetweenWhenPresent<Integer> cond = SqlBuilder.isBetweenWhenPresent(() -> (Integer) null).and(() -> null);
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value1);
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value2);
         assertThat(cond.isEmpty()).isTrue();
     }
 
@@ -63,17 +58,9 @@ void testIsNotBetween() {
         assertThat(cond.isEmpty()).isFalse();
     }
 
-    @Test
-    void testIsNotBetweenNull() {
-        IsNotBetween<Integer> cond = SqlBuilder.isNotBetween(() -> (Integer) null).and(() -> null);
-        assertThat(cond.value1()).isNull();
-        assertThat(cond.value2()).isNull();
-        assertThat(cond.isEmpty()).isFalse();
-    }
-
     @Test
     void testIsNotBetweenWhenPresent() {
-        IsNotBetween<Integer> cond = SqlBuilder.isNotBetweenWhenPresent(() -> 3).and(() -> 4);
+        IsNotBetweenWhenPresent<Integer> cond = SqlBuilder.isNotBetweenWhenPresent(() -> 3).and(() -> 4);
         assertThat(cond.value1()).isEqualTo(3);
         assertThat(cond.value2()).isEqualTo(4);
         assertThat(cond.isEmpty()).isFalse();
@@ -81,23 +68,23 @@ void testIsNotBetweenWhenPresent() {
 
     @Test
     void testIsNotBetweenWhenPresentNull() {
-        IsNotBetween<Integer> cond = SqlBuilder.isNotBetweenWhenPresent(() -> (Integer) null).and(() -> null);
-        assertThat(cond.value1()).isNull();
-        assertThat(cond.value2()).isNull();
+        IsNotBetweenWhenPresent<Integer> cond = SqlBuilder.isNotBetweenWhenPresent(() -> (Integer) null).and(() -> null);
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value1);
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value2);
         assertThat(cond.isEmpty()).isTrue();
     }
 
     @Test
     void testIsEqualToWhenPresent() {
-        IsEqualTo<Integer> cond = SqlBuilder.isEqualToWhenPresent(() -> 3);
+        IsEqualToWhenPresent<Integer> cond = SqlBuilder.isEqualToWhenPresent(() -> 3);
         assertThat(cond.value()).isEqualTo(3);
         assertThat(cond.isEmpty()).isFalse();
     }
 
     @Test
     void testIsEqualToWhenPresentNull() {
-        IsEqualTo<Integer> cond = SqlBuilder.isEqualToWhenPresent(() -> null);
-        assertThat(cond.value()).isNull();
+        IsEqualToWhenPresent<Integer> cond = SqlBuilder.isEqualToWhenPresent(() -> null);
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value);
         assertThat(cond.isEmpty()).isTrue();
     }
 
@@ -108,24 +95,17 @@ void testIsNotEqualTo() {
         assertThat(cond.isEmpty()).isFalse();
     }
 
-    @Test
-    void testIsNotEqualToNull() {
-        IsNotEqualTo<Integer> cond = SqlBuilder.isNotEqualTo(() -> (Integer) null);
-        assertThat(cond.value()).isNull();
-        assertThat(cond.isEmpty()).isFalse();
-    }
-
     @Test
     void testIsNotEqualToWhenPresent() {
-        IsNotEqualTo<Integer> cond = SqlBuilder.isNotEqualToWhenPresent(() -> 3);
+        IsNotEqualToWhenPresent<Integer> cond = SqlBuilder.isNotEqualToWhenPresent(() -> 3);
         assertThat(cond.value()).isEqualTo(3);
         assertThat(cond.isEmpty()).isFalse();
     }
 
     @Test
     void testIsNotEqualToWhenPresentNull() {
-        IsNotEqualTo<Integer> cond = SqlBuilder.isNotEqualToWhenPresent(() -> null);
-        assertThat(cond.value()).isNull();
+        IsNotEqualToWhenPresent<Integer> cond = SqlBuilder.isNotEqualToWhenPresent(() -> null);
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value);
         assertThat(cond.isEmpty()).isTrue();
     }
 
@@ -136,24 +116,17 @@ void testIsGreaterThan() {
         assertThat(cond.isEmpty()).isFalse();
     }
 
-    @Test
-    void testIsGreaterThanNull() {
-        IsGreaterThan<Integer> cond = SqlBuilder.isGreaterThan(() -> (Integer) null);
-        assertThat(cond.value()).isNull();
-        assertThat(cond.isEmpty()).isFalse();
-    }
-
     @Test
     void testIsGreaterThanWhenPresent() {
-        IsGreaterThan<Integer> cond = SqlBuilder.isGreaterThanWhenPresent(() -> 3);
+        IsGreaterThanWhenPresent<Integer> cond = SqlBuilder.isGreaterThanWhenPresent(() -> 3);
         assertThat(cond.value()).isEqualTo(3);
         assertThat(cond.isEmpty()).isFalse();
     }
 
     @Test
     void testIsGreaterThanWhenPresentNull() {
-        IsGreaterThan<Integer> cond = SqlBuilder.isGreaterThanWhenPresent(() -> null);
-        assertThat(cond.value()).isNull();
+        IsGreaterThanWhenPresent<Integer> cond = SqlBuilder.isGreaterThanWhenPresent(() -> null);
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value);
         assertThat(cond.isEmpty()).isTrue();
     }
 
@@ -164,24 +137,17 @@ void testIsGreaterThanOrEqualTo() {
         assertThat(cond.isEmpty()).isFalse();
     }
 
-    @Test
-    void testIsGreaterThanOrEqualToNull() {
-        IsGreaterThanOrEqualTo<Integer> cond = SqlBuilder.isGreaterThanOrEqualTo(() -> (Integer) null);
-        assertThat(cond.value()).isNull();
-        assertThat(cond.isEmpty()).isFalse();
-    }
-
     @Test
     void testIsGreaterThanOrEqualToWhenPresent() {
-        IsGreaterThanOrEqualTo<Integer> cond = SqlBuilder.isGreaterThanOrEqualToWhenPresent(() -> 3);
+        IsGreaterThanOrEqualToWhenPresent<Integer> cond = SqlBuilder.isGreaterThanOrEqualToWhenPresent(() -> 3);
         assertThat(cond.value()).isEqualTo(3);
         assertThat(cond.isEmpty()).isFalse();
     }
 
     @Test
     void testIsGreaterThanOrEqualToWhenPresentNull() {
-        IsGreaterThanOrEqualTo<Integer> cond = SqlBuilder.isGreaterThanOrEqualToWhenPresent(() -> null);
-        assertThat(cond.value()).isNull();
+        IsGreaterThanOrEqualToWhenPresent<Integer> cond = SqlBuilder.isGreaterThanOrEqualToWhenPresent(() -> null);
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value);
         assertThat(cond.isEmpty()).isTrue();
     }
 
@@ -192,24 +158,17 @@ void testIsLessThan() {
         assertThat(cond.isEmpty()).isFalse();
     }
 
-    @Test
-    void testIsLessThanNull() {
-        IsLessThan<Integer> cond = SqlBuilder.isLessThan(() -> (Integer) null);
-        assertThat(cond.value()).isNull();
-        assertThat(cond.isEmpty()).isFalse();
-    }
-
     @Test
     void testIsLessThanWhenPresent() {
-        IsLessThan<Integer> cond = SqlBuilder.isLessThanWhenPresent(() -> 3);
+        IsLessThanWhenPresent<Integer> cond = SqlBuilder.isLessThanWhenPresent(() -> 3);
         assertThat(cond.value()).isEqualTo(3);
         assertThat(cond.isEmpty()).isFalse();
     }
 
     @Test
     void testIsLessThanWhenPresentNull() {
-        IsLessThan<Integer> cond = SqlBuilder.isLessThanWhenPresent(() -> null);
-        assertThat(cond.value()).isNull();
+        IsLessThanWhenPresent<Integer> cond = SqlBuilder.isLessThanWhenPresent(() -> null);
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value);
         assertThat(cond.isEmpty()).isTrue();
     }
 
@@ -220,24 +179,17 @@ void testIsLessThanOrEqualTo() {
         assertThat(cond.isEmpty()).isFalse();
     }
 
-    @Test
-    void testIsLessThanOrEqualToNull() {
-        IsLessThanOrEqualTo<Integer> cond = SqlBuilder.isLessThanOrEqualTo(() -> (Integer) null);
-        assertThat(cond.value()).isNull();
-        assertThat(cond.isEmpty()).isFalse();
-    }
-
     @Test
     void testIsLessThanOrEqualToWhenPresent() {
-        IsLessThanOrEqualTo<Integer> cond = SqlBuilder.isLessThanOrEqualToWhenPresent(() -> 3);
+        IsLessThanOrEqualToWhenPresent<Integer> cond = SqlBuilder.isLessThanOrEqualToWhenPresent(() -> 3);
         assertThat(cond.value()).isEqualTo(3);
         assertThat(cond.isEmpty()).isFalse();
     }
 
     @Test
     void testIsLessThanOrEqualToWhenPresentNull() {
-        IsLessThanOrEqualTo<Integer> cond = SqlBuilder.isLessThanOrEqualToWhenPresent(() -> null);
-        assertThat(cond.value()).isNull();
+        IsLessThanOrEqualToWhenPresent<Integer> cond = SqlBuilder.isLessThanOrEqualToWhenPresent(() -> null);
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value);
         assertThat(cond.isEmpty()).isTrue();
     }
 
@@ -248,52 +200,38 @@ void testIsLike() {
         assertThat(cond.isEmpty()).isFalse();
     }
 
-    @Test
-    void testIsLikeNull() {
-        IsLike<String> cond = SqlBuilder.isLike(() -> null);
-        assertThat(cond.value()).isNull();
-        assertThat(cond.isEmpty()).isFalse();
-    }
-
     @Test
     void testIsLikeCaseInsensitive() {
-        IsLikeCaseInsensitive cond = SqlBuilder.isLikeCaseInsensitive(() -> "%f%");
+        IsLikeCaseInsensitive<String> cond = SqlBuilder.isLikeCaseInsensitive(() -> "%f%");
         assertThat(cond.value()).isEqualTo("%F%");
         assertThat(cond.isEmpty()).isFalse();
     }
 
-    @Test
-    void testIsLikeCaseInsensitiveNull() {
-        IsLikeCaseInsensitive cond = SqlBuilder.isLikeCaseInsensitive(() -> null);
-        assertThat(cond.value()).isNull();
-        assertThat(cond.isEmpty()).isFalse();
-    }
-
     @Test
     void testIsLikeCaseInsensitiveWhenPresent() {
-        IsLikeCaseInsensitive cond = SqlBuilder.isLikeCaseInsensitiveWhenPresent(() -> "%f%");
+        IsLikeCaseInsensitiveWhenPresent<String> cond = SqlBuilder.isLikeCaseInsensitiveWhenPresent(() -> "%f%");
         assertThat(cond.value()).isEqualTo("%F%");
         assertThat(cond.isEmpty()).isFalse();
     }
 
     @Test
     void testIsLikeCaseInsensitiveWhenPresentNull() {
-        IsLikeCaseInsensitive cond = SqlBuilder.isLikeCaseInsensitiveWhenPresent(() -> null);
-        assertThat(cond.value()).isNull();
+        IsLikeCaseInsensitiveWhenPresent<String> cond = SqlBuilder.isLikeCaseInsensitiveWhenPresent(() -> null);
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value);
         assertThat(cond.isEmpty()).isTrue();
     }
 
     @Test
     void testIsLikeWhenPresent() {
-        IsLike<String> cond = SqlBuilder.isLikeWhenPresent(() -> "%F%");
+        IsLikeWhenPresent<String> cond = SqlBuilder.isLikeWhenPresent(() -> "%F%");
         assertThat(cond.value()).isEqualTo("%F%");
         assertThat(cond.isEmpty()).isFalse();
     }
 
     @Test
     void testIsLikeWhenPresentNull() {
-        IsLike<String> cond = SqlBuilder.isLikeWhenPresent(() -> null);
-        assertThat(cond.value()).isNull();
+        IsLikeWhenPresent<String> cond = SqlBuilder.isLikeWhenPresent(() -> null);
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value);
         assertThat(cond.isEmpty()).isTrue();
     }
 
@@ -304,52 +242,38 @@ void testIsNotLike() {
         assertThat(cond.isEmpty()).isFalse();
     }
 
-    @Test
-    void testIsNotLikeNull() {
-        IsNotLike<String> cond = SqlBuilder.isNotLike(() -> null);
-        assertThat(cond.value()).isNull();
-        assertThat(cond.isEmpty()).isFalse();
-    }
-
     @Test
     void testIsNotLikeWhenPresent() {
-        IsNotLike<String> cond = SqlBuilder.isNotLikeWhenPresent(() -> "%F%");
+        IsNotLikeWhenPresent<String> cond = SqlBuilder.isNotLikeWhenPresent(() -> "%F%");
         assertThat(cond.value()).isEqualTo("%F%");
         assertThat(cond.isEmpty()).isFalse();
     }
 
     @Test
     void testIsNotLikeWhenPresentNull() {
-        IsNotLike<String> cond = SqlBuilder.isNotLikeWhenPresent(() -> null);
-        assertThat(cond.value()).isNull();
+        IsNotLikeWhenPresent<String> cond = SqlBuilder.isNotLikeWhenPresent(() -> null);
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value);
         assertThat(cond.isEmpty()).isTrue();
     }
 
     @Test
     void testIsNotLikeCaseInsensitive() {
-        IsNotLikeCaseInsensitive cond = SqlBuilder.isNotLikeCaseInsensitive(() -> "%f%");
+        IsNotLikeCaseInsensitive<String> cond = SqlBuilder.isNotLikeCaseInsensitive(() -> "%f%");
         assertThat(cond.value()).isEqualTo("%F%");
         assertThat(cond.isEmpty()).isFalse();
     }
 
-    @Test
-    void testIsNotLikeCaseInsensitiveNull() {
-        IsNotLikeCaseInsensitive cond = SqlBuilder.isNotLikeCaseInsensitive(() -> null);
-        assertThat(cond.value()).isNull();
-        assertThat(cond.isEmpty()).isFalse();
-    }
-
     @Test
     void testIsNotLikeCaseInsensitiveWhenPresent() {
-        IsNotLikeCaseInsensitive cond = SqlBuilder.isNotLikeCaseInsensitiveWhenPresent(() -> "%f%");
+        IsNotLikeCaseInsensitiveWhenPresent<String> cond = SqlBuilder.isNotLikeCaseInsensitiveWhenPresent(() -> "%f%");
         assertThat(cond.value()).isEqualTo("%F%");
         assertThat(cond.isEmpty()).isFalse();
     }
 
     @Test
     void testIsNotLikeCaseInsensitiveWhenPresentNull() {
-        IsNotLikeCaseInsensitive cond = SqlBuilder.isNotLikeCaseInsensitiveWhenPresent(() -> null);
-        assertThat(cond.value()).isNull();
+        IsNotLikeCaseInsensitiveWhenPresent<String> cond = SqlBuilder.isNotLikeCaseInsensitiveWhenPresent(() -> null);
+        assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(cond::value);
         assertThat(cond.isEmpty()).isTrue();
     }
 }
diff --git a/src/test/java/org/mybatis/dynamic/sql/where/render/CriterionRendererTest.java b/src/test/java/org/mybatis/dynamic/sql/where/render/CriterionRendererTest.java
index 39d297fbd..f23f6bfc5 100644
--- a/src/test/java/org/mybatis/dynamic/sql/where/render/CriterionRendererTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/where/render/CriterionRendererTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/java/org/mybatis/dynamic/sql/where/render/OptionalCriterionRenderTest.java b/src/test/java/org/mybatis/dynamic/sql/where/render/OptionalCriterionRenderTest.java
index 434155513..48bd36755 100644
--- a/src/test/java/org/mybatis/dynamic/sql/where/render/OptionalCriterionRenderTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/where/render/OptionalCriterionRenderTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,7 +19,6 @@
 import static org.assertj.core.api.Assertions.entry;
 import static org.mybatis.dynamic.sql.SqlBuilder.*;
 
-import java.util.Objects;
 import java.util.Optional;
 
 import org.junit.jupiter.api.Test;
@@ -35,21 +34,7 @@ class OptionalCriterionRenderTest {
 
     @Test
     void testNoRenderableCriteria() {
-        Integer nullId = null;
-
-        Optional<WhereClauseProvider> whereClause = where(id, isEqualToWhenPresent(nullId))
-                .configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true))
-                .build()
-                .render(RenderingStrategies.SPRING_NAMED_PARAMETER);
-
-        assertThat(whereClause).isEmpty();
-    }
-
-    @Test
-    void testNoRenderableCriteriaWithIf() {
-        Integer nullId = null;
-
-        Optional<WhereClauseProvider> whereClause = where(id, isEqualTo(nullId).filter(Objects::nonNull))
+        Optional<WhereClauseProvider> whereClause = where(id, isEqualToWhenPresent((Integer) null))
                 .configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true))
                 .build()
                 .render(RenderingStrategies.SPRING_NAMED_PARAMETER);
@@ -103,10 +88,8 @@ void testEnabledIsNotNull() {
 
     @Test
     void testOneRenderableCriteriaBeforeNull() {
-        String nullFirstName = null;
-
         Optional<WhereClauseProvider> whereClause = where(id, isEqualToWhenPresent(22))
-                .and(firstName, isEqualToWhenPresent(nullFirstName))
+                .and(firstName, isEqualToWhenPresent((String) null))
                 .build()
                 .render(RenderingStrategies.SPRING_NAMED_PARAMETER);
 
@@ -118,9 +101,7 @@ void testOneRenderableCriteriaBeforeNull() {
 
     @Test
     void testOneRenderableCriteriaBeforeNull2() {
-        String nullFirstName = null;
-
-        Optional<WhereClauseProvider> whereClause = where(id, isEqualToWhenPresent(22), and(firstName, isEqualToWhenPresent(nullFirstName)))
+        Optional<WhereClauseProvider> whereClause = where(id, isEqualToWhenPresent(22), and(firstName, isEqualToWhenPresent((String) null)))
                 .build()
                 .render(RenderingStrategies.SPRING_NAMED_PARAMETER);
 
@@ -132,9 +113,7 @@ void testOneRenderableCriteriaBeforeNull2() {
 
     @Test
     void testOneRenderableCriteriaAfterNull() {
-        Integer nullId = null;
-
-        Optional<WhereClauseProvider> whereClause = where(id, isEqualToWhenPresent(nullId))
+        Optional<WhereClauseProvider> whereClause = where(id, isEqualToWhenPresent((Integer) null))
                 .and(firstName, isEqualToWhenPresent("fred"))
                 .build()
                 .render(RenderingStrategies.SPRING_NAMED_PARAMETER);
@@ -147,9 +126,7 @@ void testOneRenderableCriteriaAfterNull() {
 
     @Test
     void testOneRenderableCriteriaAfterNull2() {
-        Integer nullId = null;
-
-        Optional<WhereClauseProvider> whereClause = where(id, isEqualToWhenPresent(nullId), and(firstName, isEqualToWhenPresent("fred")))
+        Optional<WhereClauseProvider> whereClause = where(id, isEqualToWhenPresent((Integer) null), and(firstName, isEqualToWhenPresent("fred")))
                 .build()
                 .render(RenderingStrategies.SPRING_NAMED_PARAMETER);
 
@@ -161,9 +138,7 @@ void testOneRenderableCriteriaAfterNull2() {
 
     @Test
     void testOverrideFirstConnector() {
-        Integer nullId = null;
-
-        Optional<WhereClauseProvider> whereClause = where(id, isEqualToWhenPresent(nullId), and(firstName, isEqualToWhenPresent("fred")), or(lastName, isEqualTo("flintstone")))
+        Optional<WhereClauseProvider> whereClause = where(id, isEqualToWhenPresent((Integer) null), and(firstName, isEqualToWhenPresent("fred")), or(lastName, isEqualTo("flintstone")))
                 .build()
                 .render(RenderingStrategies.SPRING_NAMED_PARAMETER);
 
@@ -306,10 +281,8 @@ void testWhereExistsAndAnd() {
 
     @Test
     void testCollapsingCriteriaGroup1() {
-        String name1 = null;
-
         Optional<WhereClauseProvider> whereClause = where(
-                group(firstName, isEqualToWhenPresent(name1)), or(lastName, isEqualToWhenPresent(name1)))
+                group(firstName, isEqualToWhenPresent((String) null)), or(lastName, isEqualToWhenPresent((String) null)))
                 .configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true))
                 .build()
                 .render(RenderingStrategies.SPRING_NAMED_PARAMETER);
@@ -319,10 +292,8 @@ void testCollapsingCriteriaGroup1() {
 
     @Test
     void testCollapsingCriteriaGroup2() {
-        String name1 = null;
-
         Optional<WhereClauseProvider> whereClause = where(
-                group(firstName, isEqualTo("Fred")), or(lastName, isEqualToWhenPresent(name1)))
+                group(firstName, isEqualTo("Fred")), or(lastName, isEqualToWhenPresent((String) null)))
                 .build()
                 .render(RenderingStrategies.SPRING_NAMED_PARAMETER);
 
@@ -336,10 +307,8 @@ void testCollapsingCriteriaGroup2() {
 
     @Test
     void testCollapsingCriteriaGroup3() {
-        String name1 = null;
-
         Optional<WhereClauseProvider> whereClause = where(
-                group(firstName, isEqualTo("Fred")), or(lastName, isEqualToWhenPresent(name1)), or(firstName, isEqualTo("Betty")))
+                group(firstName, isEqualTo("Fred")), or(lastName, isEqualToWhenPresent((String) null)), or(firstName, isEqualTo("Betty")))
                 .build()
                 .render(RenderingStrategies.SPRING_NAMED_PARAMETER);
 
diff --git a/src/test/java/org/mybatis/dynamic/sql/where/render/RenderedCriterionTest.java b/src/test/java/org/mybatis/dynamic/sql/where/render/RenderedCriterionTest.java
index b99112298..2ab3299ee 100644
--- a/src/test/java/org/mybatis/dynamic/sql/where/render/RenderedCriterionTest.java
+++ b/src/test/java/org/mybatis/dynamic/sql/where/render/RenderedCriterionTest.java
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/animal/data/AnimalDataDynamicSqlSupport.kt b/src/test/kotlin/examples/kotlin/animal/data/AnimalDataDynamicSqlSupport.kt
index e5cedd8a6..6c3e67e5a 100644
--- a/src/test/kotlin/examples/kotlin/animal/data/AnimalDataDynamicSqlSupport.kt
+++ b/src/test/kotlin/examples/kotlin/animal/data/AnimalDataDynamicSqlSupport.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt b/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt
index 8f6eb7aa8..673265487 100644
--- a/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt
+++ b/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -922,6 +922,17 @@ class KCaseExpressionTest {
         }.withMessage(Messages.getString("ERROR.41"))
     }
 
+    @Test
+    fun testInvalidMissingThen() {
+        assertThatExceptionOfType(KInvalidSQLException::class.java).isThrownBy {
+            case {
+                `when` {
+                    id isEqualTo 22
+                }
+            }
+        }.withMessage(Messages.getString("ERROR.47"))
+    }
+
     @Test
     fun testInvalidSearchedMissingWhen() {
         assertThatExceptionOfType(InvalidSqlException::class.java).isThrownBy {
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/TestUtils.kt b/src/test/kotlin/examples/kotlin/mybatis3/TestUtils.kt
index 8cd230607..d07943fb0 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/TestUtils.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/TestUtils.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/canonical/AddressDynamicSqlSupport.kt b/src/test/kotlin/examples/kotlin/mybatis3/canonical/AddressDynamicSqlSupport.kt
index 728f5fbc0..74e7f3e3e 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/canonical/AddressDynamicSqlSupport.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/canonical/AddressDynamicSqlSupport.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/canonical/AddressMapper.kt b/src/test/kotlin/examples/kotlin/mybatis3/canonical/AddressMapper.kt
index 4efc72ca5..262421dda 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/canonical/AddressMapper.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/canonical/AddressMapper.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/canonical/AddressRecord.kt b/src/test/kotlin/examples/kotlin/mybatis3/canonical/AddressRecord.kt
index 25976b0a0..50bbcbd4c 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/canonical/AddressRecord.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/canonical/AddressRecord.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/canonical/GeneratedAlwaysDynamicSqlSupport.kt b/src/test/kotlin/examples/kotlin/mybatis3/canonical/GeneratedAlwaysDynamicSqlSupport.kt
index b297e10d5..78b7fe1cc 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/canonical/GeneratedAlwaysDynamicSqlSupport.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/canonical/GeneratedAlwaysDynamicSqlSupport.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/canonical/GeneratedAlwaysMapper.kt b/src/test/kotlin/examples/kotlin/mybatis3/canonical/GeneratedAlwaysMapper.kt
index 236ad4f00..2d8aaacd8 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/canonical/GeneratedAlwaysMapper.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/canonical/GeneratedAlwaysMapper.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/canonical/GeneratedAlwaysRecord.kt b/src/test/kotlin/examples/kotlin/mybatis3/canonical/GeneratedAlwaysRecord.kt
index 8856bcda2..8e0700d7b 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/canonical/GeneratedAlwaysRecord.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/canonical/GeneratedAlwaysRecord.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/canonical/GeneratedAlwaysTest.kt b/src/test/kotlin/examples/kotlin/mybatis3/canonical/GeneratedAlwaysTest.kt
index 39bcd2e1c..0f17275ee 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/canonical/GeneratedAlwaysTest.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/canonical/GeneratedAlwaysTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/canonical/LastName.kt b/src/test/kotlin/examples/kotlin/mybatis3/canonical/LastName.kt
index 57454f205..a5d12e6ba 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/canonical/LastName.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/canonical/LastName.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/canonical/LastNameTypeHandler.kt b/src/test/kotlin/examples/kotlin/mybatis3/canonical/LastNameTypeHandler.kt
index dd175cafd..a209dcb6f 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/canonical/LastNameTypeHandler.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/canonical/LastNameTypeHandler.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonDynamicSqlSupport.kt b/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonDynamicSqlSupport.kt
index cac3e5461..94e60161d 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonDynamicSqlSupport.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonDynamicSqlSupport.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapper.kt b/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapper.kt
index e7df3fd61..d1b177db7 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapper.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapper.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapperExtensions.kt b/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapperExtensions.kt
index ee86408f8..237b5ebd7 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapperExtensions.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapperExtensions.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapperTest.kt b/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapperTest.kt
index 8276ebe9c..86af992a7 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapperTest.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapperTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -35,13 +35,18 @@ import org.junit.jupiter.api.TestInstance
 import org.junit.jupiter.api.TestInstance.Lifecycle
 import org.mybatis.dynamic.sql.exception.NonRenderingWhereClauseException
 import org.mybatis.dynamic.sql.util.kotlin.elements.add
+import org.mybatis.dynamic.sql.util.kotlin.elements.case
+import org.mybatis.dynamic.sql.util.kotlin.elements.concat
 import org.mybatis.dynamic.sql.util.kotlin.elements.constant
 import org.mybatis.dynamic.sql.util.kotlin.elements.isIn
 import org.mybatis.dynamic.sql.util.kotlin.elements.sortColumn
+import org.mybatis.dynamic.sql.util.kotlin.elements.stringConstant
+import org.mybatis.dynamic.sql.util.kotlin.elements.sum
 import org.mybatis.dynamic.sql.util.kotlin.mybatis3.insertInto
 import org.mybatis.dynamic.sql.util.kotlin.mybatis3.insertSelect
 import org.mybatis.dynamic.sql.util.kotlin.mybatis3.multiSelect
 import org.mybatis.dynamic.sql.util.kotlin.mybatis3.select
+import org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper
 import java.util.*
 
 @TestInstance(Lifecycle.PER_CLASS)
@@ -55,6 +60,7 @@ class PersonMapperTest {
             withMapper(PersonMapper::class)
             withMapper(PersonWithAddressMapper::class)
             withMapper(AddressMapper::class)
+            withMapper(CommonSelectMapper::class)
             withTypeHandler(YesNoTypeHandler::class)
         }
     }
@@ -242,7 +248,7 @@ class PersonMapperTest {
     }
 
     @Test
-    fun testInsertSelect() {
+    fun testInsertSelectExtensionFunction() {
         sqlSessionFactory.openSession().use { session ->
             val mapper = session.getMapper(PersonMapper::class.java)
 
@@ -259,7 +265,7 @@ class PersonMapperTest {
     }
 
     @Test
-    fun testDeprecatedInsertSelect() {
+    fun testInsertSelect() {
         sqlSessionFactory.openSession().use { session ->
             val mapper = session.getMapper(PersonMapper::class.java)
 
@@ -907,14 +913,47 @@ class PersonMapperTest {
     }
 
     @Test
-    fun testRenderingEmptyList() {
-        val selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) {
-            from(person)
-            where { id isIn emptyList() }
+    fun testSumWithCase() {
+        sqlSessionFactory.openSession().use { session ->
+            val mapper = session.getMapper(CommonSelectMapper::class.java)
+
+            val selectStatement = select(id, sum(case {
+                `when` {
+                    id isEqualTo 1
+                    then(101)
+                }
+                `else`(999)
+            }).`as`("fred")) {
+                from(person)
+                groupBy(id)
+            }
+
+            val expected =
+                "select id, sum(case when id = #{parameters.p1,jdbcType=INTEGER} then 101 else 999 end) as fred from Person group by id"
+            assertThat(selectStatement.selectStatement).isEqualTo(expected)
+
+            val rows = mapper.selectManyMappedRows(selectStatement)
+            assertThat(rows).hasSize(6)
         }
+    }
 
-        val expected = "select id, first_name, last_name, birth_date, employed, occupation, address_id from Person " +
-                "where id in ()"
-        assertThat(selectStatement.selectStatement).isEqualTo(expected)
+    @Test
+    fun testConcat() {
+        sqlSessionFactory.openSession().use { session ->
+            val mapper = session.getMapper(CommonSelectMapper::class.java)
+
+            val selectStatement = select(id, concat(firstName, stringConstant(" "), lastName).`as`("fullname")) {
+                from(person)
+                where { concat(firstName, stringConstant(" "), lastName) isEqualTo "Fred Flintstone" }
+            }
+
+            val expected =
+                "select id, concat(first_name, ' ', last_name) as fullname from Person " +
+                        "where concat(first_name, ' ', last_name) = #{parameters.p1,jdbcType=VARCHAR}"
+            assertThat(selectStatement.selectStatement).isEqualTo(expected)
+
+            val rows = mapper.selectManyMappedRows(selectStatement)
+            assertThat(rows).hasSize(1)
+        }
     }
 }
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonRecord.kt b/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonRecord.kt
index 3d641e8ad..3fee94072 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonRecord.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonRecord.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonWithAddress.kt b/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonWithAddress.kt
index a2dc9102e..546bfb795 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonWithAddress.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonWithAddress.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonWithAddressMapper.kt b/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonWithAddressMapper.kt
index f45e228ac..534b50336 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonWithAddressMapper.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonWithAddressMapper.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonWithAddressMapperExtensions.kt b/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonWithAddressMapperExtensions.kt
index c24c3a967..7c8fcf21e 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonWithAddressMapperExtensions.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonWithAddressMapperExtensions.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -35,8 +35,8 @@ private val columnList = listOf(id `as` "A_ID", firstName, lastName, birthDate,
 fun PersonWithAddressMapper.selectOne(completer: SelectCompleter): PersonWithAddress? =
     select(columnList) {
         from(person)
-        fullJoin(address) {
-            on(person.addressId) equalTo address.id
+        fullJoin(address) on {
+            person.addressId isEqualTo address.id
         }
         completer()
     }.run(this::selectOne)
@@ -44,8 +44,8 @@ fun PersonWithAddressMapper.selectOne(completer: SelectCompleter): PersonWithAdd
 fun PersonWithAddressMapper.select(completer: SelectCompleter): List<PersonWithAddress> =
     select(columnList) {
         from(person, "p")
-        fullJoin(address) {
-            on(person.addressId) equalTo address.id
+        fullJoin(address) on {
+            person.addressId isEqualTo address.id
         }
         completer()
     }.run(this::selectMany)
@@ -53,8 +53,8 @@ fun PersonWithAddressMapper.select(completer: SelectCompleter): List<PersonWithA
 fun PersonWithAddressMapper.selectDistinct(completer: SelectCompleter): List<PersonWithAddress> =
     selectDistinct(columnList) {
         from(person)
-        fullJoin(address) {
-            on(person.addressId) equalTo address.id
+        fullJoin(address) on {
+            person.addressId isEqualTo address.id
         }
         completer()
     }.run(this::selectMany)
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/canonical/ReusableWhereTest.kt b/src/test/kotlin/examples/kotlin/mybatis3/canonical/ReusableWhereTest.kt
index 7feca7e63..9ddc5706c 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/canonical/ReusableWhereTest.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/canonical/ReusableWhereTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -27,7 +27,6 @@ import org.junit.jupiter.api.BeforeAll
 import org.junit.jupiter.api.Test
 import org.junit.jupiter.api.TestInstance
 import org.mybatis.dynamic.sql.util.kotlin.GroupingCriteriaCollector.Companion.where
-import org.mybatis.dynamic.sql.util.kotlin.WhereApplier
 import org.mybatis.dynamic.sql.util.kotlin.andThen
 import org.mybatis.dynamic.sql.util.kotlin.mybatis3.select
 
@@ -44,19 +43,6 @@ class ReusableWhereTest {
         }
     }
 
-    @Test
-    fun testCountWithDeprecatedClause() {
-        sqlSessionFactory.openSession().use { session ->
-            val mapper = session.getMapper(PersonMapper::class.java)
-
-            val rows = mapper.count {
-                applyWhere(commonWhere)
-            }
-
-            assertThat(rows).isEqualTo(3)
-        }
-    }
-
     @Test
     fun testDelete() {
         sqlSessionFactory.openSession().use { session ->
@@ -98,27 +84,6 @@ class ReusableWhereTest {
         }
     }
 
-    @Test
-    fun testDeprecatedComposition() {
-        val composedWhereClause = commonWhere.andThen {
-            and { birthDate.isNotNull() }
-        }.andThen {
-            or { addressId isLessThan 3 }
-        }
-
-        val selectStatement = select(person.allColumns()) {
-            from(person)
-            applyWhere(composedWhereClause)
-        }
-
-        assertThat(selectStatement.selectStatement).isEqualTo(
-            "select * from Person " +
-                "where id = #{parameters.p1,jdbcType=INTEGER} or occupation is null " +
-                "and birth_date is not null " +
-                "or address_id < #{parameters.p2,jdbcType=INTEGER}"
-        )
-    }
-
     @Test
     fun testComposition() {
         val composedWhereClause = commonWhereClause.andThen {
@@ -140,11 +105,6 @@ class ReusableWhereTest {
         )
     }
 
-    private val commonWhere: WhereApplier = {
-        where { id isEqualTo 1 }
-        or { occupation.isNull() }
-    }
-
     private val commonWhereClause = where {
         id isEqualTo 1
         or { occupation.isNull() }
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/canonical/YesNoTypeHandler.kt b/src/test/kotlin/examples/kotlin/mybatis3/canonical/YesNoTypeHandler.kt
index 6b41308e0..46c264d04 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/canonical/YesNoTypeHandler.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/canonical/YesNoTypeHandler.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/column/comparison/ColumnComparisonConfiguration.kt b/src/test/kotlin/examples/kotlin/mybatis3/column/comparison/ColumnComparisonConfiguration.kt
index 9491717c1..a472c4647 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/column/comparison/ColumnComparisonConfiguration.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/column/comparison/ColumnComparisonConfiguration.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/column/comparison/ColumnComparisonDynamicSqlSupport.kt b/src/test/kotlin/examples/kotlin/mybatis3/column/comparison/ColumnComparisonDynamicSqlSupport.kt
index c49f9aae6..5d00df6b2 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/column/comparison/ColumnComparisonDynamicSqlSupport.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/column/comparison/ColumnComparisonDynamicSqlSupport.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/column/comparison/ColumnComparisonMapper.kt b/src/test/kotlin/examples/kotlin/mybatis3/column/comparison/ColumnComparisonMapper.kt
index 657bc9b66..6ad3e205f 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/column/comparison/ColumnComparisonMapper.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/column/comparison/ColumnComparisonMapper.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/column/comparison/ColumnComparisonRecord.kt b/src/test/kotlin/examples/kotlin/mybatis3/column/comparison/ColumnComparisonRecord.kt
index dcbd79661..95170c2d6 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/column/comparison/ColumnComparisonRecord.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/column/comparison/ColumnComparisonRecord.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/column/comparison/ColumnComparisonTest.kt b/src/test/kotlin/examples/kotlin/mybatis3/column/comparison/ColumnComparisonTest.kt
index ef4208ed8..3fe95f434 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/column/comparison/ColumnComparisonTest.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/column/comparison/ColumnComparisonTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/custom/render/KCustomRenderingTest.kt b/src/test/kotlin/examples/kotlin/mybatis3/custom/render/KCustomRenderingTest.kt
index 04c359fac..47a6fcaed 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/custom/render/KCustomRenderingTest.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/custom/render/KCustomRenderingTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -304,7 +304,7 @@ class KCustomRenderingTest {
         }
     }
 
-    private fun <T> dereference(column: SqlColumn<T>, attribute: String) =
+    private fun <T : Any> dereference(column: SqlColumn<T>, attribute: String) =
         SqlColumn.of<String>(column.name() + "->>'" + attribute + "'", column.table(), JDBCType.VARCHAR)
 
     companion object {
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/custom/render/KJsonRenderingStrategy.kt b/src/test/kotlin/examples/kotlin/mybatis3/custom/render/KJsonRenderingStrategy.kt
index 70cfe012b..9441f7fd8 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/custom/render/KJsonRenderingStrategy.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/custom/render/KJsonRenderingStrategy.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/custom/render/KJsonTestDynamicSqlSupport.kt b/src/test/kotlin/examples/kotlin/mybatis3/custom/render/KJsonTestDynamicSqlSupport.kt
index eb9e444a0..2b12d483e 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/custom/render/KJsonTestDynamicSqlSupport.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/custom/render/KJsonTestDynamicSqlSupport.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/custom/render/KJsonTestMapper.kt b/src/test/kotlin/examples/kotlin/mybatis3/custom/render/KJsonTestMapper.kt
index d1b2a30b6..5145e18b8 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/custom/render/KJsonTestMapper.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/custom/render/KJsonTestMapper.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/custom/render/KJsonTestRecord.kt b/src/test/kotlin/examples/kotlin/mybatis3/custom/render/KJsonTestRecord.kt
index 8256ac847..7c2dfb856 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/custom/render/KJsonTestRecord.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/custom/render/KJsonTestRecord.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/general/GeneralKotlinTest.kt b/src/test/kotlin/examples/kotlin/mybatis3/general/GeneralKotlinTest.kt
index 68d073cc9..5184e3410 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/general/GeneralKotlinTest.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/general/GeneralKotlinTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -367,8 +367,8 @@ class GeneralKotlinTest {
                 address.id, address.streetAddress, address.city, address.state
             ) {
                 from(person)
-                join(address) {
-                    on(addressId) equalTo address.id
+                join(address) on {
+                    addressId isEqualTo address.id
                 }
                 where { id isLessThan 4 }
                 orderBy(id)
@@ -403,8 +403,8 @@ class GeneralKotlinTest {
                 address.id, address.streetAddress, address.city, address.state
             ) {
                 from(person)
-                join(address) {
-                    on(addressId) equalTo address.id
+                join(address) on {
+                    addressId isEqualTo address.id
                 }
                 where {
                     id isLessThan 5
@@ -446,8 +446,8 @@ class GeneralKotlinTest {
                 address.id, address.streetAddress, address.city, address.state
             ) {
                 from(person)
-                join(address) {
-                    on(addressId) equalTo address.id
+                join(address) on {
+                    addressId isEqualTo address.id
                 }
                 where {
                     id isEqualTo 5
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/general/KGroupingTest.kt b/src/test/kotlin/examples/kotlin/mybatis3/general/KGroupingTest.kt
index 015859094..91bb853f1 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/general/KGroupingTest.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/general/KGroupingTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/joins/DeprecatedJoinMapperTest.kt b/src/test/kotlin/examples/kotlin/mybatis3/joins/DeprecatedJoinMapperTest.kt
new file mode 100644
index 000000000..d712c46c6
--- /dev/null
+++ b/src/test/kotlin/examples/kotlin/mybatis3/joins/DeprecatedJoinMapperTest.kt
@@ -0,0 +1,604 @@
+/*
+ *    Copyright 2016-2025 the original author or authors.
+ *
+ *    Licensed under the Apache License, Version 2.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 examples.kotlin.mybatis3.joins
+
+import examples.kotlin.mybatis3.TestUtils
+import examples.kotlin.mybatis3.joins.ItemMasterDynamicSQLSupport.itemMaster
+import examples.kotlin.mybatis3.joins.OrderDetailDynamicSQLSupport.orderDetail
+import examples.kotlin.mybatis3.joins.OrderLineDynamicSQLSupport.orderLine
+import examples.kotlin.mybatis3.joins.OrderMasterDynamicSQLSupport.orderMaster
+import examples.kotlin.mybatis3.joins.UserDynamicSQLSupport.user
+import org.apache.ibatis.session.SqlSessionFactory
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.assertThatExceptionOfType
+import org.assertj.core.api.Assertions.entry
+import org.junit.jupiter.api.BeforeAll
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.TestInstance
+import org.mybatis.dynamic.sql.util.Messages
+import org.mybatis.dynamic.sql.util.kotlin.KInvalidSQLException
+import org.mybatis.dynamic.sql.util.kotlin.elements.invoke
+import org.mybatis.dynamic.sql.util.kotlin.mybatis3.select
+
+@Suppress("LargeClass")
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class DeprecatedJoinMapperTest {
+    private lateinit var sqlSessionFactory: SqlSessionFactory
+
+    @BeforeAll
+    fun setup() {
+        sqlSessionFactory = TestUtils.buildSqlSessionFactory {
+            withInitializationScript("/examples/kotlin/mybatis3/joins/CreateJoinDB.sql")
+            withMapper(JoinMapper::class)
+        }
+    }
+
+    @Test
+    fun testSingleTableJoinWithValue() {
+        sqlSessionFactory.openSession().use { session ->
+            val mapper = session.getMapper(JoinMapper::class.java)
+
+            val selectStatement = select(
+                orderMaster.orderId, orderMaster.orderDate,
+                orderDetail.lineNumber, orderDetail.description, orderDetail.quantity
+            ) {
+                from(orderMaster)
+                join(orderDetail) {
+                    on(orderMaster.orderId) equalTo orderDetail.orderId
+                    and(orderMaster.orderId) equalTo 1
+                }
+            }
+
+            val expectedStatement = "select OrderMaster.order_id, OrderMaster.order_date, OrderDetail.line_number, OrderDetail.description, OrderDetail.quantity" +
+                    " from OrderMaster join OrderDetail on OrderMaster.order_id = OrderDetail.order_id" +
+                    " and OrderMaster.order_id = #{parameters.p1,jdbcType=INTEGER}"
+
+            assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement)
+
+            val rows = mapper.selectMany(selectStatement)
+
+            assertThat(rows).hasSize(1)
+
+            with(rows[0]) {
+                assertThat(id).isEqualTo(1)
+                assertThat(details).hasSize(2)
+                assertThat(details?.get(0)?.lineNumber).isEqualTo(1)
+                assertThat(details?.get(1)?.lineNumber).isEqualTo(2)
+            }
+        }
+    }
+
+    @Test
+    fun testCompoundJoin1() {
+        // this is a nonsensical join, but it does test the "and" capability
+        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 orderDetail.orderId
+            }
+        }
+
+        val expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" +
+            " from OrderMaster om join OrderDetail od on om.order_id = od.order_id and om.order_id = od.order_id"
+        assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement)
+    }
+
+    @Test
+    fun testFullJoinWithAliases() {
+        sqlSessionFactory.openSession().use { session ->
+            val mapper = session.getMapper(JoinMapper::class.java)
+
+            val selectStatement = select(
+                orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description
+            ) {
+                from(orderMaster, "om")
+                join(orderLine, "ol") {
+                    on(orderMaster.orderId) equalTo orderLine.orderId
+                }
+                fullJoin(itemMaster, "im") {
+                    on(orderLine.itemId) equalTo itemMaster.itemId
+                }
+                orderBy(orderLine.orderId, itemMaster.itemId)
+            }
+
+            val expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" +
+                " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" +
+                " full join ItemMaster im on ol.item_id = im.item_id" +
+                " order by order_id, item_id"
+
+            assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement)
+
+            data class OrderDetail(val itemId: Int?, val orderId: Int?, val quantity: Int?, val description: String?)
+
+            val rows = mapper.selectMany(selectStatement) {
+                OrderDetail(
+                    it["ITEM_ID"] as Int?,
+                    it["ORDER_ID"] as Int?,
+                    it["QUANTITY"] as Int?,
+                    it["DESCRIPTION"] as String?
+                )
+            }
+
+            assertThat(rows).hasSize(6)
+
+            with(rows[0]) {
+                assertThat(itemId).isEqualTo(55)
+                assertThat(orderId).isNull()
+                assertThat(quantity).isNull()
+                assertThat(description).isEqualTo("Catcher Glove")
+            }
+
+            with(rows[3]) {
+                assertThat(itemId).isNull()
+                assertThat(orderId).isEqualTo(2)
+                assertThat(quantity).isEqualTo(6)
+                assertThat(description).isNull()
+            }
+
+            with(rows[5]) {
+                assertThat(itemId).isEqualTo(44)
+                assertThat(orderId).isEqualTo(2)
+                assertThat(quantity).isEqualTo(1)
+                assertThat(description).isEqualTo("Outfield Glove")
+            }
+        }
+    }
+
+    @Test
+    @Suppress("LongMethod")
+    fun testFullJoinWithSubQuery() {
+        sqlSessionFactory.openSession().use { session ->
+            val mapper = session.getMapper(JoinMapper::class.java)
+
+            val selectStatement = select(
+                "ol"(orderLine.orderId), orderLine.quantity, "im"(itemMaster.itemId),
+                itemMaster.description
+            ) {
+                from {
+                    select(orderMaster.allColumns()) {
+                        from(orderMaster)
+                    }
+                    + "om"
+                }
+                join(
+                    subQuery = {
+                        select(orderLine.allColumns()) {
+                            from(orderLine)
+                        }
+                        + "ol"
+                    },
+                    joinCriteria = {
+                        on("om"(orderMaster.orderId)) equalTo
+                                "ol"(orderLine.orderId)
+                    }
+                )
+                fullJoin(
+                    {
+                        select(itemMaster.allColumns()) {
+                            from(itemMaster)
+                        }
+                        + "im"
+                    }
+                ) {
+                    on("ol"(orderLine.itemId)) equalTo
+                            "im"(itemMaster.itemId)
+                }
+                orderBy(orderLine.orderId, itemMaster.itemId)
+            }
+
+            val expectedStatement = "select ol.order_id, quantity, im.item_id, description" +
+                " from (select * from OrderMaster) om" +
+                " join (select * from OrderLine) ol on om.order_id = ol.order_id" +
+                " full join (select * from ItemMaster) im on ol.item_id = im.item_id" +
+                " order by order_id, item_id"
+
+            assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement)
+
+            data class OrderDetail(val itemId: Int?, val orderId: Int?, val quantity: Int?, val description: String?)
+
+            val rows = mapper.selectMany(selectStatement) {
+                OrderDetail(
+                    it["ITEM_ID"] as Int?,
+                    it["ORDER_ID"] as Int?,
+                    it["QUANTITY"] as Int?,
+                    it["DESCRIPTION"] as String?
+                )
+            }
+
+            assertThat(rows).hasSize(6)
+
+            with(rows[0]) {
+                assertThat(itemId).isEqualTo(55)
+                assertThat(orderId).isNull()
+                assertThat(quantity).isNull()
+                assertThat(description).isEqualTo("Catcher Glove")
+            }
+
+            with(rows[3]) {
+                assertThat(itemId).isNull()
+                assertThat(orderId).isEqualTo(2)
+                assertThat(quantity).isEqualTo(6)
+                assertThat(description).isNull()
+            }
+
+            with(rows[5]) {
+                assertThat(itemId).isEqualTo(44)
+                assertThat(orderId).isEqualTo(2)
+                assertThat(quantity).isEqualTo(1)
+                assertThat(description).isEqualTo("Outfield Glove")
+            }
+        }
+    }
+
+    @Test
+    fun testFullJoinWithoutAliases() {
+        sqlSessionFactory.openSession().use { session ->
+            val mapper = session.getMapper(JoinMapper::class.java)
+
+            val selectStatement = select(
+                orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description
+            ) {
+                from(orderMaster, "om")
+                join(orderLine, "ol") {
+                    on(orderMaster.orderId) equalTo orderLine.orderId
+                }
+                fullJoin(itemMaster) {
+                    on(orderLine.itemId) equalTo itemMaster.itemId
+                }
+                orderBy(orderLine.orderId, itemMaster.itemId)
+            }
+
+            val expectedStatement = "select ol.order_id, ol.quantity, ItemMaster.item_id, ItemMaster.description" +
+                " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" +
+                " full join ItemMaster on ol.item_id = ItemMaster.item_id" +
+                " order by order_id, item_id"
+
+            assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement)
+
+            val rows = mapper.selectManyMappedRows(selectStatement)
+
+            assertThat(rows).hasSize(6)
+
+            assertThat(rows[0]).containsExactly(
+                entry("DESCRIPTION", "Catcher Glove"),
+                entry("ITEM_ID", 55)
+            )
+
+            assertThat(rows[3]).containsExactly(
+                entry("ORDER_ID", 2),
+                entry("QUANTITY", 6)
+            )
+
+            assertThat(rows[5]).containsExactly(
+                entry("ORDER_ID", 2),
+                entry("QUANTITY", 1),
+                entry("DESCRIPTION", "Outfield Glove"),
+                entry("ITEM_ID", 44)
+            )
+        }
+    }
+
+    @Test
+    fun testLeftJoinWithAliases() {
+        sqlSessionFactory.openSession().use { session ->
+            val mapper = session.getMapper(JoinMapper::class.java)
+
+            val selectStatement = select(
+                orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description
+            ) {
+                from(orderMaster, "om")
+                join(orderLine, "ol") {
+                    on(orderMaster.orderId) equalTo orderLine.orderId
+                }
+                leftJoin(itemMaster, "im") {
+                    on(orderLine.itemId) equalTo itemMaster.itemId
+                }
+                orderBy(orderLine.orderId, itemMaster.itemId)
+            }
+
+            val expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" +
+                " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" +
+                " left join ItemMaster im on ol.item_id = im.item_id" +
+                " order by order_id, item_id"
+
+            assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement)
+
+            val rows = mapper.selectManyMappedRows(selectStatement)
+
+            assertThat(rows).hasSize(5)
+
+            assertThat(rows[2]).containsExactly(
+                entry("ORDER_ID", 2),
+                entry("QUANTITY", 6)
+            )
+
+            assertThat(rows[4]).containsExactly(
+                entry("ORDER_ID", 2),
+                entry("QUANTITY", 1),
+                entry("DESCRIPTION", "Outfield Glove"),
+                entry("ITEM_ID", 44)
+            )
+        }
+    }
+
+    @Test
+    fun testLeftJoinWithSubQuery() {
+        sqlSessionFactory.openSession().use { session ->
+            val mapper = session.getMapper(JoinMapper::class.java)
+
+            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)
+            }
+
+            val expectedStatement = "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"
+
+            assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement)
+
+            val rows = mapper.selectManyMappedRows(selectStatement)
+
+            assertThat(rows).hasSize(5)
+
+            assertThat(rows[2]).containsExactly(
+                entry("ORDER_ID", 2),
+                entry("QUANTITY", 6)
+            )
+
+            assertThat(rows[4]).containsExactly(
+                entry("ORDER_ID", 2),
+                entry("QUANTITY", 1),
+                entry("DESCRIPTION", "Outfield Glove"),
+                entry("ITEM_ID", 44)
+            )
+        }
+    }
+
+    @Test
+    fun testLeftJoinWithoutAliases() {
+        sqlSessionFactory.openSession().use { session ->
+            val mapper = session.getMapper(JoinMapper::class.java)
+
+            val selectStatement = select(
+                orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description
+            ) {
+                from(orderMaster, "om")
+                join(orderLine, "ol") {
+                    on(orderMaster.orderId) equalTo orderLine.orderId
+                }
+                leftJoin(itemMaster) {
+                    on(orderLine.itemId)  equalTo itemMaster.itemId
+                }
+                orderBy(orderLine.orderId, itemMaster.itemId)
+            }
+
+            val expectedStatement = "select ol.order_id, ol.quantity, ItemMaster.item_id, ItemMaster.description" +
+                " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" +
+                " left join ItemMaster on ol.item_id = ItemMaster.item_id" +
+                " order by order_id, item_id"
+
+            assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement)
+
+            val rows = mapper.selectManyMappedRows(selectStatement)
+
+            assertThat(rows).hasSize(5)
+
+            assertThat(rows[2]).containsExactly(
+                entry("ORDER_ID", 2),
+                entry("QUANTITY", 6)
+            )
+
+            assertThat(rows[4]).containsExactly(
+                entry("ORDER_ID", 2),
+                entry("QUANTITY", 1),
+                entry("DESCRIPTION", "Outfield Glove"),
+                entry("ITEM_ID", 44)
+            )
+        }
+    }
+
+    @Test
+    fun testRightJoinWithAliases() {
+        sqlSessionFactory.openSession().use { session ->
+            val mapper = session.getMapper(JoinMapper::class.java)
+
+            val selectStatement = select(
+                orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description
+            ) {
+                from(orderMaster, "om")
+                join(orderLine, "ol") {
+                    on(orderMaster.orderId) equalTo orderLine.orderId
+                }
+                rightJoin(itemMaster, "im") {
+                    on(orderLine.itemId) equalTo itemMaster.itemId
+                }
+                orderBy(orderLine.orderId, itemMaster.itemId)
+            }
+
+            val expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" +
+                " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" +
+                " right join ItemMaster im on ol.item_id = im.item_id" +
+                " order by order_id, item_id"
+
+            assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement)
+
+            val rows = mapper.selectManyMappedRows(selectStatement)
+
+            assertThat(rows).hasSize(5)
+
+            assertThat(rows[0]).containsExactly(
+                entry("DESCRIPTION", "Catcher Glove"),
+                entry("ITEM_ID", 55)
+            )
+
+            assertThat(rows[4]).containsExactly(
+                entry("ORDER_ID", 2),
+                entry("QUANTITY", 1),
+                entry("DESCRIPTION", "Outfield Glove"),
+                entry("ITEM_ID", 44)
+            )
+        }
+    }
+
+    @Test
+    fun testRightJoinWithSubQuery() {
+        sqlSessionFactory.openSession().use { session ->
+            val mapper = session.getMapper(JoinMapper::class.java)
+
+            val selectStatement = select(
+                orderLine.orderId, orderLine.quantity,
+                "im"(itemMaster.itemId), itemMaster.description
+            ) {
+                from(orderMaster, "om")
+                join(orderLine, "ol") {
+                    on(orderMaster.orderId) equalTo orderLine.orderId
+                }
+                rightJoin(
+                    {
+                        select(itemMaster.allColumns()) {
+                            from(itemMaster)
+                        }
+                        + "im"
+                    }
+                ) {
+                    on(orderLine.itemId) equalTo "im"(itemMaster.itemId)
+                }
+                orderBy(orderLine.orderId, itemMaster.itemId)
+            }
+
+            val expectedStatement = "select ol.order_id, ol.quantity, im.item_id, description" +
+                " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" +
+                " right join (select * from ItemMaster) im on ol.item_id = im.item_id" +
+                " order by order_id, item_id"
+
+            assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement)
+
+            val rows = mapper.selectManyMappedRows(selectStatement)
+
+            assertThat(rows).hasSize(5)
+
+            assertThat(rows[0]).containsExactly(
+                entry("DESCRIPTION", "Catcher Glove"),
+                entry("ITEM_ID", 55)
+            )
+
+            assertThat(rows[4]).containsExactly(
+                entry("ORDER_ID", 2),
+                entry("QUANTITY", 1),
+                entry("DESCRIPTION", "Outfield Glove"),
+                entry("ITEM_ID", 44)
+            )
+        }
+    }
+
+    @Test
+    fun testRightJoinWithoutAliases() {
+        sqlSessionFactory.openSession().use { session ->
+            val mapper = session.getMapper(JoinMapper::class.java)
+
+            val selectStatement = select(
+                orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description
+            ) {
+                from(orderMaster, "om")
+                join(orderLine, "ol") {
+                    on(orderMaster.orderId) equalTo orderLine.orderId
+                }
+                rightJoin(itemMaster) {
+                    on(orderLine.itemId) equalTo itemMaster.itemId
+                }
+                orderBy(orderLine.orderId, itemMaster.itemId)
+            }
+
+            val expectedStatement = "select ol.order_id, ol.quantity, ItemMaster.item_id, ItemMaster.description" +
+                " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" +
+                " right join ItemMaster on ol.item_id = ItemMaster.item_id" +
+                " order by order_id, item_id"
+
+            assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement)
+
+            val rows = mapper.selectManyMappedRows(selectStatement)
+
+            assertThat(rows).hasSize(5)
+
+            assertThat(rows[0]).containsExactly(
+                entry("DESCRIPTION", "Catcher Glove"),
+                entry("ITEM_ID", 55)
+            )
+
+            assertThat(rows[4]).containsExactly(
+                entry("ORDER_ID", 2),
+                entry("QUANTITY", 1),
+                entry("DESCRIPTION", "Outfield Glove"),
+                entry("ITEM_ID", 44)
+            )
+        }
+    }
+
+    @Test
+    fun testJoinWithNoOnCondition() {
+        // create second table instance for self-join
+        val user2 = user.withAlias("other_user")
+
+        assertThatExceptionOfType(KInvalidSQLException::class.java).isThrownBy {
+            select(user.userId, user.userName, user.parentId) {
+                from(user, "u1")
+                join(user2, "u2") {
+                    and(user.userId) equalTo user2.parentId
+                }
+                where { user2.userId isEqualTo 4 }
+            }
+        }.withMessage(Messages.getString("ERROR.22")) //$NON-NLS-1$
+    }
+
+    @Test
+    fun testJoinWithDoubleOnCondition() {
+        // create second table instance for self-join
+        val user2 = user.withAlias("other_user")
+
+        assertThatExceptionOfType(KInvalidSQLException::class.java).isThrownBy {
+            select(user.userId, user.userName, user.parentId) {
+                from(user, "u1")
+                join(user2, "u2") {
+                    on(user.userId) equalTo user2.parentId
+                    on(user.userId) equalTo user2.parentId
+                }
+                where { user2.userId isEqualTo 4 }
+            }
+        }.withMessage(Messages.getString("ERROR.45")) //$NON-NLS-1$
+    }
+}
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/joins/Domain.kt b/src/test/kotlin/examples/kotlin/mybatis3/joins/Domain.kt
index 39f767485..db6543a26 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/joins/Domain.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/joins/Domain.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/joins/ExistsTest.kt b/src/test/kotlin/examples/kotlin/mybatis3/joins/ExistsTest.kt
index c49ac3ae4..a763ce6fe 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/joins/ExistsTest.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/joins/ExistsTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/joins/ItemMasterDynamicSQLSupport.kt b/src/test/kotlin/examples/kotlin/mybatis3/joins/ItemMasterDynamicSQLSupport.kt
index 274542121..5a08ff9e8 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/joins/ItemMasterDynamicSQLSupport.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/joins/ItemMasterDynamicSQLSupport.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapper.kt b/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapper.kt
index 116fc7091..053066593 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapper.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapper.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperTest.kt b/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperNewSyntaxTest.kt
similarity index 82%
rename from src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperTest.kt
rename to src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperNewSyntaxTest.kt
index a60bc2105..1ad6d71c2 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperTest.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperNewSyntaxTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -32,12 +32,11 @@ import org.mybatis.dynamic.sql.util.Messages
 import org.mybatis.dynamic.sql.util.kotlin.KInvalidSQLException
 import org.mybatis.dynamic.sql.util.kotlin.elements.constant
 import org.mybatis.dynamic.sql.util.kotlin.elements.invoke
-import org.mybatis.dynamic.sql.util.kotlin.elements.max
 import org.mybatis.dynamic.sql.util.kotlin.mybatis3.select
 
 @Suppress("LargeClass")
 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
-class JoinMapperTest {
+class JoinMapperNewSyntaxTest {
     private lateinit var sqlSessionFactory: SqlSessionFactory
 
     @BeforeAll
@@ -58,8 +57,8 @@ class JoinMapperTest {
                 orderDetail.lineNumber, orderDetail.description, orderDetail.quantity
             ) {
                 from(orderMaster, "om")
-                join(orderDetail, "od") {
-                    on(orderMaster.orderId) equalTo orderDetail.orderId
+                join(orderDetail, "od") on {
+                    orderMaster.orderId isEqualTo orderDetail.orderId
                 }
             }
 
@@ -97,9 +96,9 @@ class JoinMapperTest {
                 orderDetail.lineNumber, orderDetail.description, orderDetail.quantity
             ) {
                 from(orderMaster, "om")
-                join(orderDetail, "od") {
-                    on(orderMaster.orderId) equalTo orderDetail.orderId
-                    and(orderMaster.orderId) equalTo 1
+                join(orderDetail, "od") on {
+                    orderMaster.orderId isEqualTo orderDetail.orderId
+                    and { orderMaster.orderId isEqualTo 1 }
                 }
             }
 
@@ -132,9 +131,9 @@ class JoinMapperTest {
                 orderDetail.lineNumber, orderDetail.description, orderDetail.quantity
             ) {
                 from(orderMaster, "om")
-                join(orderDetail, "od") {
-                    on(orderMaster.orderId) equalTo orderDetail.orderId
-                    and(orderMaster.orderId) equalTo constant("1")
+                join(orderDetail, "od") on {
+                    orderMaster.orderId isEqualTo orderDetail.orderId
+                    and { orderMaster.orderId isEqualTo constant<Int>("1") }
                 }
             }
 
@@ -165,9 +164,9 @@ class JoinMapperTest {
             orderDetail.description, orderDetail.quantity
         ) {
             from(orderMaster, "om")
-            join(orderDetail, "od") {
-                on(orderMaster.orderId) equalTo orderDetail.orderId
-                and(orderMaster.orderId) equalTo orderDetail.orderId
+            join(orderDetail, "od") on {
+                orderMaster.orderId isEqualTo orderDetail.orderId
+                and { orderMaster.orderId isEqualTo orderDetail.orderId }
             }
         }
 
@@ -184,9 +183,9 @@ class JoinMapperTest {
             orderDetail.description, orderDetail.quantity
         ) {
             from(orderMaster, "om")
-            join(orderDetail, "od") {
-                on(orderMaster.orderId) equalTo orderDetail.orderId
-                and(orderMaster.orderId) equalTo orderDetail.orderId
+            join(orderDetail, "od") on {
+                orderMaster.orderId isEqualTo orderDetail.orderId
+                and { orderMaster.orderId isEqualTo orderDetail.orderId }
             }
             where { orderMaster.orderId isEqualTo 1 }
         }
@@ -207,11 +206,11 @@ class JoinMapperTest {
                 itemMaster.description, orderLine.quantity
             ) {
                 from(orderMaster, "om")
-                join(orderLine, "ol") {
-                    on(orderMaster.orderId) equalTo orderLine.orderId
+                join(orderLine, "ol") on {
+                    orderMaster.orderId isEqualTo orderLine.orderId
                 }
-                join(itemMaster, "im") {
-                    on(orderLine.itemId) equalTo itemMaster.itemId
+                join(itemMaster, "im") on {
+                    orderLine.itemId isEqualTo itemMaster.itemId
                 }
                 where { orderMaster.orderId isEqualTo 2 }
             }
@@ -243,11 +242,11 @@ class JoinMapperTest {
                 orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description
             ) {
                 from(orderMaster, "om")
-                join(orderLine, "ol") {
-                    on(orderMaster.orderId) equalTo orderLine.orderId
+                join(orderLine, "ol") on {
+                    orderMaster.orderId isEqualTo orderLine.orderId
                 }
-                fullJoin(itemMaster, "im") {
-                    on(orderLine.itemId) equalTo itemMaster.itemId
+                fullJoin(itemMaster, "im") on {
+                    orderLine.itemId isEqualTo itemMaster.itemId
                 }
                 orderBy(orderLine.orderId, itemMaster.itemId)
             }
@@ -311,28 +310,21 @@ class JoinMapperTest {
                     }
                     + "om"
                 }
-                join(
-                    subQuery = {
-                        select(orderLine.allColumns()) {
-                            from(orderLine)
-                        }
-                        + "ol"
-                    },
-                    joinCriteria = {
-                        on("om"(orderMaster.orderId)) equalTo
-                                "ol"(orderLine.orderId)
+                join {
+                    select(orderLine.allColumns()) {
+                        from(orderLine)
                     }
-                )
-                fullJoin(
-                    {
-                        select(itemMaster.allColumns()) {
-                            from(itemMaster)
-                        }
-                        + "im"
+                    + "ol"
+                } on {
+                    "om"(orderMaster.orderId) isEqualTo "ol"(orderLine.orderId)
+                }
+                fullJoin {
+                    select(itemMaster.allColumns()) {
+                        from(itemMaster)
                     }
-                ) {
-                    on("ol"(orderLine.itemId)) equalTo
-                            "im"(itemMaster.itemId)
+                    +"im"
+                } on {
+                    "ol"(orderLine.itemId) isEqualTo "im"(itemMaster.itemId)
                 }
                 orderBy(orderLine.orderId, itemMaster.itemId)
             }
@@ -390,11 +382,11 @@ class JoinMapperTest {
                 orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description
             ) {
                 from(orderMaster, "om")
-                join(orderLine, "ol") {
-                    on(orderMaster.orderId) equalTo orderLine.orderId
+                join(orderLine, "ol") on {
+                    orderMaster.orderId isEqualTo orderLine.orderId
                 }
-                fullJoin(itemMaster) {
-                    on(orderLine.itemId) equalTo itemMaster.itemId
+                fullJoin(itemMaster) on {
+                    orderLine.itemId isEqualTo itemMaster.itemId
                 }
                 orderBy(orderLine.orderId, itemMaster.itemId)
             }
@@ -438,11 +430,11 @@ class JoinMapperTest {
                 orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description
             ) {
                 from(orderMaster, "om")
-                join(orderLine, "ol") {
-                    on(orderMaster.orderId) equalTo orderLine.orderId
+                join(orderLine, "ol") on {
+                    orderMaster.orderId isEqualTo orderLine.orderId
                 }
-                leftJoin(itemMaster, "im") {
-                    on(orderLine.itemId) equalTo itemMaster.itemId
+                leftJoin(itemMaster, "im") on {
+                    orderLine.itemId isEqualTo itemMaster.itemId
                 }
                 orderBy(orderLine.orderId, itemMaster.itemId)
             }
@@ -482,18 +474,16 @@ class JoinMapperTest {
                 itemMaster.description
             ) {
                 from(orderMaster, "om")
-                join(orderLine, "ol") {
-                    on(orderMaster.orderId) equalTo orderLine.orderId
+                join(orderLine, "ol") on {
+                    orderMaster.orderId isEqualTo orderLine.orderId
                 }
-                leftJoin(
-                    {
-                        select(itemMaster.allColumns()) {
-                            from(itemMaster)
-                        }
-                        + "im"
+                leftJoin {
+                    select(itemMaster.allColumns()) {
+                        from(itemMaster)
                     }
-                ) {
-                    on(orderLine.itemId) equalTo "im"(itemMaster.itemId)
+                    +"im"
+                } on {
+                    orderLine.itemId isEqualTo "im"(itemMaster.itemId)
                 }
                 orderBy(orderLine.orderId, itemMaster.itemId)
             }
@@ -532,11 +522,11 @@ class JoinMapperTest {
                 orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description
             ) {
                 from(orderMaster, "om")
-                join(orderLine, "ol") {
-                    on(orderMaster.orderId) equalTo orderLine.orderId
+                join(orderLine, "ol") on {
+                    orderMaster.orderId isEqualTo orderLine.orderId
                 }
-                leftJoin(itemMaster) {
-                    on(orderLine.itemId)  equalTo itemMaster.itemId
+                leftJoin(itemMaster) on {
+                    orderLine.itemId isEqualTo itemMaster.itemId
                 }
                 orderBy(orderLine.orderId, itemMaster.itemId)
             }
@@ -575,11 +565,11 @@ class JoinMapperTest {
                 orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description
             ) {
                 from(orderMaster, "om")
-                join(orderLine, "ol") {
-                    on(orderMaster.orderId) equalTo orderLine.orderId
+                join(orderLine, "ol") on {
+                    orderMaster.orderId isEqualTo orderLine.orderId
                 }
-                rightJoin(itemMaster, "im") {
-                    on(orderLine.itemId) equalTo itemMaster.itemId
+                rightJoin(itemMaster, "im") on {
+                    orderLine.itemId isEqualTo itemMaster.itemId
                 }
                 orderBy(orderLine.orderId, itemMaster.itemId)
             }
@@ -619,18 +609,16 @@ class JoinMapperTest {
                 "im"(itemMaster.itemId), itemMaster.description
             ) {
                 from(orderMaster, "om")
-                join(orderLine, "ol") {
-                    on(orderMaster.orderId) equalTo orderLine.orderId
+                join(orderLine, "ol") on {
+                    orderMaster.orderId isEqualTo orderLine.orderId
                 }
-                rightJoin(
-                    {
-                        select(itemMaster.allColumns()) {
-                            from(itemMaster)
-                        }
-                        + "im"
+                rightJoin {
+                    select(itemMaster.allColumns()) {
+                        from(itemMaster)
                     }
-                ) {
-                    on(orderLine.itemId) equalTo "im"(itemMaster.itemId)
+                    +"im"
+                } on {
+                    orderLine.itemId isEqualTo "im"(itemMaster.itemId)
                 }
                 orderBy(orderLine.orderId, itemMaster.itemId)
             }
@@ -669,11 +657,11 @@ class JoinMapperTest {
                 orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description
             ) {
                 from(orderMaster, "om")
-                join(orderLine, "ol") {
-                    on(orderMaster.orderId) equalTo orderLine.orderId
+                join(orderLine, "ol") on {
+                    orderMaster.orderId isEqualTo orderLine.orderId
                 }
-                rightJoin(itemMaster) {
-                    on(orderLine.itemId) equalTo itemMaster.itemId
+                rightJoin(itemMaster) on {
+                    orderLine.itemId isEqualTo itemMaster.itemId
                 }
                 orderBy(orderLine.orderId, itemMaster.itemId)
             }
@@ -714,8 +702,8 @@ class JoinMapperTest {
             // get Bamm Bamm's parent - should be Barney
             val selectStatement = select(user.userId, user.userName, user.parentId) {
                 from(user, "u1")
-                join(user2, "u2") {
-                    on(user.userId) equalTo user2.parentId
+                join(user2, "u2") on {
+                    user.userId isEqualTo user2.parentId
                 }
                 where { user2.userId isEqualTo 4 }
             }
@@ -745,8 +733,8 @@ class JoinMapperTest {
             // get Bamm Bamm's parent - should be Barney
             val selectStatement = select(user.userId, user.userName, user.parentId) {
                 from(user)
-                join(user2) {
-                    on(user.userId) equalTo user2.parentId
+                join(user2) on {
+                    user.userId isEqualTo user2.parentId
                 }
                 where { user2.userId isEqualTo 4 }
             }
@@ -777,8 +765,8 @@ class JoinMapperTest {
             // get Bamm Bamm's parent - should be Barney
             val selectStatement = select(user.userId, user.userName, user.parentId) {
                 from(user, "u1")
-                join(user2, "u2") {
-                    on(user.userId) equalTo user2.parentId
+                join(user2, "u2") on {
+                    user.userId isEqualTo user2.parentId
                 }
                 where { user2.userId isEqualTo 4 }
             }
@@ -798,62 +786,47 @@ class JoinMapperTest {
     }
 
     @Test
-    fun testJoinWithNoOnCondition() {
-        // create second table instance for self-join
-        val user2 = user.withAlias("other_user")
-
-        assertThatExceptionOfType(KInvalidSQLException::class.java).isThrownBy {
-            select(user.userId, user.userName, user.parentId) {
-                from(user, "u1")
-                join(user2, "u2") {
-                    and(user.userId) equalTo user2.parentId
-                }
-                where { user2.userId isEqualTo 4 }
-            }
-        }.withMessage(Messages.getString("ERROR.22")) //$NON-NLS-1$
-    }
-
-    @Test
-    fun testThatAliasesPropagateToSubQueryConditions() {
+    fun testSelfWithNewAliasAndOverrideOddUsage() {
         sqlSessionFactory.openSession().use { session ->
             val mapper = session.getMapper(JoinMapper::class.java)
 
-            val orderLine2 = OrderLineDynamicSQLSupport.OrderLine()
+            // create second table instance for self-join
+            val user2 = user.withAlias("other_user")
 
-            val selectStatement = select(orderLine.orderId, orderLine.lineNumber) {
-                from(orderLine, "ol")
-                where {
-                    orderLine.lineNumber isEqualTo  {
-                        select(max(orderLine2.lineNumber)) {
-                            from(orderLine2, "ol2")
-                            where { orderLine2.orderId isEqualTo orderLine.orderId }
-                        }
-                    }
+            // get Bamm Bamm's parent - should be Barney
+            val selectStatement = select(user.userId, user.userName, user.parentId) {
+                from(user, "u1")
+                join(user2, "u2") on {
+                    and { user.userId isEqualTo user2.parentId }
                 }
-                orderBy(orderLine.orderId)
+                where { user2.userId isEqualTo 4 }
             }
 
-            val expectedStatement = "select ol.order_id, ol.line_number " +
-                    "from OrderLine ol " +
-                    "where ol.line_number = " +
-                    "(select max(ol2.line_number) from OrderLine ol2 where ol2.order_id = ol.order_id) " +
-                    "order by order_id"
-
+            val expectedStatement = "select u1.user_id, u1.user_name, u1.parent_id" +
+                    " from User u1 join User u2 on u1.user_id = u2.parent_id" +
+                    " where u2.user_id = #{parameters.p1,jdbcType=INTEGER}"
             assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement)
-
             val rows = mapper.selectManyMappedRows(selectStatement)
+            assertThat(rows).hasSize(1)
 
-            assertThat(rows).hasSize(2)
-
-            assertThat(rows[0]).containsOnly(
-                entry("ORDER_ID", 1),
-                entry("LINE_NUMBER", 2)
-            )
-
-            assertThat(rows[1]).containsOnly(
-                entry("ORDER_ID", 2),
-                entry("LINE_NUMBER", 3)
+            assertThat(rows[0]).containsExactly(
+                entry("USER_ID", 2),
+                entry("USER_NAME", "Barney"),
             )
         }
     }
+
+    @Test
+    fun testJoinWithNoOnCondition() {
+        // create second table instance for self-join
+        val user2 = user.withAlias("other_user")
+
+        assertThatExceptionOfType(KInvalidSQLException::class.java).isThrownBy {
+            select(user.userId, user.userName, user.parentId) {
+                from(user, "u1")
+                join(user2, "u2") on { }
+                where { user2.userId isEqualTo 4 }
+            }
+        }.withMessage(Messages.getString("ERROR.22")) //$NON-NLS-1$
+    }
 }
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/joins/OrderDetailDynamicSQLSupport.kt b/src/test/kotlin/examples/kotlin/mybatis3/joins/OrderDetailDynamicSQLSupport.kt
index 4dea828e1..1d65414f3 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/joins/OrderDetailDynamicSQLSupport.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/joins/OrderDetailDynamicSQLSupport.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/joins/OrderLineDynamicSQLSupport.kt b/src/test/kotlin/examples/kotlin/mybatis3/joins/OrderLineDynamicSQLSupport.kt
index 6f4e0bc49..a457f7e33 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/joins/OrderLineDynamicSQLSupport.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/joins/OrderLineDynamicSQLSupport.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/joins/OrderMasterDynamicSQLSupport.kt b/src/test/kotlin/examples/kotlin/mybatis3/joins/OrderMasterDynamicSQLSupport.kt
index 283ecea43..fc785718a 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/joins/OrderMasterDynamicSQLSupport.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/joins/OrderMasterDynamicSQLSupport.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/joins/UserDynamicSQLSupport.kt b/src/test/kotlin/examples/kotlin/mybatis3/joins/UserDynamicSQLSupport.kt
index 78fa830b7..1d307b6c4 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/joins/UserDynamicSQLSupport.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/joins/UserDynamicSQLSupport.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KIsLikeEscape.kt b/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KIsLikeEscape.kt
new file mode 100644
index 000000000..6746dc03e
--- /dev/null
+++ b/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KIsLikeEscape.kt
@@ -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 examples.kotlin.mybatis3.mariadb
+
+import java.util.function.Predicate
+import java.util.function.Function
+import org.mybatis.dynamic.sql.AbstractSingleValueCondition
+import org.mybatis.dynamic.sql.BindableColumn
+import org.mybatis.dynamic.sql.render.RenderingContext
+import org.mybatis.dynamic.sql.util.FragmentAndParameters
+
+sealed class KIsLikeEscape<T : Any>(
+    value: T,
+    private val escapeCharacter: Char? = null
+) : AbstractSingleValueCondition<T>(value), AbstractSingleValueCondition.Filterable<T>,
+    AbstractSingleValueCondition.Mappable<T> {
+
+    override fun operator(): String = "like"
+
+    override fun renderCondition(
+        renderingContext: RenderingContext,
+        leftColumn: BindableColumn<T>
+    ): FragmentAndParameters = with(super.renderCondition(renderingContext, leftColumn)) {
+        escapeCharacter?.let { mapFragment { "$it ESCAPE '$escapeCharacter'" } } ?: this
+    }
+
+    override fun filter(predicate: Predicate<in T>): KIsLikeEscape<T> =
+        filterSupport(predicate, EmptyIsLikeEscape::empty, this)
+
+    override fun <R : Any> map(mapper : Function<in T, out R>): KIsLikeEscape<R> =
+        mapSupport(mapper, { r -> ConcreteIsLikeEscape(r, escapeCharacter) }, EmptyIsLikeEscape::empty)
+
+    companion object {
+        fun <T: Any> isLike(value: T, escapeCharacter: Char? = null) : KIsLikeEscape<T> =
+            ConcreteIsLikeEscape(value, escapeCharacter)
+    }
+}
+
+private class ConcreteIsLikeEscape<T: Any>(
+    value: T,
+    escapeCharacter: Char? = null
+) : KIsLikeEscape<T>(value, escapeCharacter)
+
+private class EmptyIsLikeEscape : KIsLikeEscape<Any>(-1) {
+    override fun isEmpty(): Boolean = true
+
+    override fun value(): Any {
+        throw NoSuchElementException("No value present")
+    }
+
+    companion object {
+        private val EMPTY: KIsLikeEscape<Any> = EmptyIsLikeEscape()
+
+        @Suppress("UNCHECKED_CAST")
+        internal fun <T : Any> empty(): KIsLikeEscape<T> = EMPTY as KIsLikeEscape<T>
+    }
+}
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KItemsDynamicSQLSupport.kt b/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KItemsDynamicSQLSupport.kt
index 9d306f25e..3ca4e4b8a 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KItemsDynamicSQLSupport.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KItemsDynamicSQLSupport.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KMariaDBTest.kt b/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KMariaDBTest.kt
index 5782dad97..28bf98a3b 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KMariaDBTest.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KMariaDBTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -38,6 +38,7 @@ import org.mybatis.dynamic.sql.util.mybatis3.CommonUpdateMapper
 import org.testcontainers.containers.MariaDBContainer
 import org.testcontainers.junit.jupiter.Container
 import org.testcontainers.junit.jupiter.Testcontainers
+import java.util.Locale
 
 @Testcontainers
 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
@@ -142,6 +143,92 @@ class KMariaDBTest {
         }
     }
 
+
+    /**
+     * Shortcut function for KIsLikeEscape
+     *
+     * Note that the following example uses of this function are a bit awkward and don't look as natural as the
+     * built-in conditions. We should be able to improve this once Kotlin implements the context parameters
+     * proposal (https://github.com/Kotlin/KEEP/issues/367)
+     */
+    fun <T : Any> isLike(value: T, escapeCharacter: Char? = null) = KIsLikeEscape.isLike(value, escapeCharacter)
+
+    @Test
+    fun testIsLikeEscape() {
+        sqlSessionFactory.openSession().use { session ->
+            val mapper = session.getMapper(CommonSelectMapper::class.java)
+            val selectStatement = select(id, description) {
+                from(items)
+                where {
+                    description(isLike("Item 1%", '#'))
+                }
+            }
+
+            assertThat(selectStatement.selectStatement).isEqualTo("select id, description from items where description like #{parameters.p1,jdbcType=VARCHAR} ESCAPE '#'")
+            assertThat(selectStatement.parameters).containsEntry("p1", "Item 1%")
+
+            val rows = mapper.selectManyMappedRows(selectStatement)
+            assertThat(rows).hasSize(11)
+        }
+    }
+
+    @Test
+    fun testIsLikeEscapeNoEscapeCharacter() {
+        val selectStatement = select(id, description) {
+            from(items)
+            where {
+                description(isLike("%fred%"))
+            }
+        }
+
+        assertThat(selectStatement.selectStatement).isEqualTo("select id, description from items where description like #{parameters.p1,jdbcType=VARCHAR}")
+        assertThat(selectStatement.parameters).containsEntry("p1", "%fred%")
+    }
+
+    @Test
+    fun testIsLikeEscapeMap() {
+        val selectStatement = select(id, description) {
+            from(items)
+            where {
+                description(isLike("%fred%", '#').map { s -> s.uppercase(Locale.getDefault()) })
+            }
+        }
+
+        assertThat(selectStatement.selectStatement).isEqualTo("select id, description from items where description like #{parameters.p1,jdbcType=VARCHAR} ESCAPE '#'")
+        assertThat(selectStatement.parameters).containsEntry("p1", "%FRED%")
+    }
+
+    @Test
+    fun testIsLikeEscapeFilter() {
+        val selectStatement = select(id, description) {
+            from(items)
+            where {
+                description(isLike("%fred%", '#').filter { _ -> false })
+            }
+            configureStatement { isNonRenderingWhereClauseAllowed = true }
+        }
+
+        assertThat(selectStatement.selectStatement).isEqualTo("select id, description from items")
+        assertThat(selectStatement.parameters).isEmpty()
+    }
+
+    @Test
+    fun testIsLikeEscapeFilterMapFilter() {
+        val selectStatement = select(id, description) {
+            from(items)
+            where {
+                description(isLike("%fred%", '#')
+                    .filter { _ -> true }
+                    .map { s -> s.uppercase(Locale.getDefault()) }
+                    .filter{_ -> false })
+            }
+            configureStatement { isNonRenderingWhereClauseAllowed = true }
+        }
+
+        assertThat(selectStatement.selectStatement).isEqualTo("select id, description from items")
+        assertThat(selectStatement.parameters).isEmpty()
+    }
+
     companion object {
         @Container
         private val mariadb = MariaDBContainer(TestContainersConfiguration.MARIADB_LATEST)
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/sharding/KShardedMapper.kt b/src/test/kotlin/examples/kotlin/mybatis3/sharding/KShardedMapper.kt
index 230b7f4b5..73af69c00 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/sharding/KShardedMapper.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/sharding/KShardedMapper.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/sharding/KShardingTest.kt b/src/test/kotlin/examples/kotlin/mybatis3/sharding/KShardingTest.kt
index 19e8d23ed..3af1ca0f8 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/sharding/KShardingTest.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/sharding/KShardingTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/mybatis3/sharding/KTableCodesTableDynamicSQLSupport.kt b/src/test/kotlin/examples/kotlin/mybatis3/sharding/KTableCodesTableDynamicSQLSupport.kt
index 7f4eda634..c3e16901e 100644
--- a/src/test/kotlin/examples/kotlin/mybatis3/sharding/KTableCodesTableDynamicSQLSupport.kt
+++ b/src/test/kotlin/examples/kotlin/mybatis3/sharding/KTableCodesTableDynamicSQLSupport.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/AddressDynamicSqlSupport.kt b/src/test/kotlin/examples/kotlin/spring/canonical/AddressDynamicSqlSupport.kt
index 8323960ef..355aeeae4 100644
--- a/src/test/kotlin/examples/kotlin/spring/canonical/AddressDynamicSqlSupport.kt
+++ b/src/test/kotlin/examples/kotlin/spring/canonical/AddressDynamicSqlSupport.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTemplateDirectTest.kt b/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTemplateDirectTest.kt
index 7157e709a..6f558a346 100644
--- a/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTemplateDirectTest.kt
+++ b/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTemplateDirectTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -577,8 +577,8 @@ open class CanonicalSpringKotlinTemplateDirectTest {
             address.id, address.streetAddress, address.city, address.state
         ) {
             from(person, "p")
-            join(address, "a") {
-                on(addressId) equalTo address.id
+            join(address, "a") on {
+                addressId isEqualTo address.id
             }
             where { id isLessThan 4 }
             orderBy(id)
diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTest.kt b/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTest.kt
index 7b9093892..ae51e6972 100644
--- a/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTest.kt
+++ b/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -1122,8 +1122,8 @@ open class CanonicalSpringKotlinTest {
             address.streetAddress, address.city, address.state
         ) {
             from(person, "p")
-            join(address, "a") {
-                on(addressId) equalTo address.id
+            join(address, "a") on {
+                addressId isEqualTo address.id
             }
             where { id isLessThan 4 }
             orderBy(id)
diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/CompoundKeyDynamicSqlSupport.kt b/src/test/kotlin/examples/kotlin/spring/canonical/CompoundKeyDynamicSqlSupport.kt
index e44a1edef..fb18b48ce 100644
--- a/src/test/kotlin/examples/kotlin/spring/canonical/CompoundKeyDynamicSqlSupport.kt
+++ b/src/test/kotlin/examples/kotlin/spring/canonical/CompoundKeyDynamicSqlSupport.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/DomainAndConverters.kt b/src/test/kotlin/examples/kotlin/spring/canonical/DomainAndConverters.kt
index 6c6b64b5f..2aed4763e 100644
--- a/src/test/kotlin/examples/kotlin/spring/canonical/DomainAndConverters.kt
+++ b/src/test/kotlin/examples/kotlin/spring/canonical/DomainAndConverters.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/GeneratedAlwaysDynamicSqlSupport.kt b/src/test/kotlin/examples/kotlin/spring/canonical/GeneratedAlwaysDynamicSqlSupport.kt
index 4740791b5..0c388bdb6 100644
--- a/src/test/kotlin/examples/kotlin/spring/canonical/GeneratedAlwaysDynamicSqlSupport.kt
+++ b/src/test/kotlin/examples/kotlin/spring/canonical/GeneratedAlwaysDynamicSqlSupport.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -26,7 +26,7 @@ object GeneratedAlwaysDynamicSqlSupport {
     val fullName = generatedAlways.fullName
 
     class GeneratedAlways : SqlTable("GeneratedAlways") {
-        val id = column<Int>("id")
+        val id = column<Int>(name = "id")
         val firstName = column<String>(name = "first_name")
         val lastName = column<String>(name = "last_name")
         val fullName = column<String>(name = "full_name")
diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/InfixElementsTest.kt b/src/test/kotlin/examples/kotlin/spring/canonical/InfixElementsTest.kt
index e5fb13e4f..b1b9791f2 100644
--- a/src/test/kotlin/examples/kotlin/spring/canonical/InfixElementsTest.kt
+++ b/src/test/kotlin/examples/kotlin/spring/canonical/InfixElementsTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/InfixSubQueriesTest.kt b/src/test/kotlin/examples/kotlin/spring/canonical/InfixSubQueriesTest.kt
index f20ac37cb..37d0745ed 100644
--- a/src/test/kotlin/examples/kotlin/spring/canonical/InfixSubQueriesTest.kt
+++ b/src/test/kotlin/examples/kotlin/spring/canonical/InfixSubQueriesTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/KotlinElementsTest.kt b/src/test/kotlin/examples/kotlin/spring/canonical/KotlinElementsTest.kt
index 98e9c7a86..74c230200 100644
--- a/src/test/kotlin/examples/kotlin/spring/canonical/KotlinElementsTest.kt
+++ b/src/test/kotlin/examples/kotlin/spring/canonical/KotlinElementsTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/PersonDynamicSqlSupport.kt b/src/test/kotlin/examples/kotlin/spring/canonical/PersonDynamicSqlSupport.kt
index c0e57852a..049e276f2 100644
--- a/src/test/kotlin/examples/kotlin/spring/canonical/PersonDynamicSqlSupport.kt
+++ b/src/test/kotlin/examples/kotlin/spring/canonical/PersonDynamicSqlSupport.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/RowMappers.kt b/src/test/kotlin/examples/kotlin/spring/canonical/RowMappers.kt
index d175f49cf..e40360760 100644
--- a/src/test/kotlin/examples/kotlin/spring/canonical/RowMappers.kt
+++ b/src/test/kotlin/examples/kotlin/spring/canonical/RowMappers.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/SpringConfiguration.kt b/src/test/kotlin/examples/kotlin/spring/canonical/SpringConfiguration.kt
index d225a50db..b9c89041c 100644
--- a/src/test/kotlin/examples/kotlin/spring/canonical/SpringConfiguration.kt
+++ b/src/test/kotlin/examples/kotlin/spring/canonical/SpringConfiguration.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/SpringKotlinMapToRowTest.kt b/src/test/kotlin/examples/kotlin/spring/canonical/SpringKotlinMapToRowTest.kt
index cfb1d6388..a4e936787 100644
--- a/src/test/kotlin/examples/kotlin/spring/canonical/SpringKotlinMapToRowTest.kt
+++ b/src/test/kotlin/examples/kotlin/spring/canonical/SpringKotlinMapToRowTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/SpringKotlinSubQueryTest.kt b/src/test/kotlin/examples/kotlin/spring/canonical/SpringKotlinSubQueryTest.kt
index eb97249a6..439680dd4 100644
--- a/src/test/kotlin/examples/kotlin/spring/canonical/SpringKotlinSubQueryTest.kt
+++ b/src/test/kotlin/examples/kotlin/spring/canonical/SpringKotlinSubQueryTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -113,12 +113,12 @@ open class SpringKotlinSubQueryTest {
 
     @Test
     fun testBasicSubQueryWithAliases() {
-        val rowNum = DerivedColumn.of<Int>("rownum()") `as` "myRows"
+        val rowNum = DerivedColumn.of<Int>("rownum()")
         val outerFirstName = "b"(firstName)
         val personId = DerivedColumn.of<Int>("personId", "b")
 
         val selectStatement =
-            select(outerFirstName.asCamelCase(), personId, rowNum) {
+            select(outerFirstName.asCamelCase(), personId, rowNum `as` "myRows") {
                 from {
                     select(id `as` "personId", firstName) {
                         from(person, "a")
diff --git a/src/test/kotlin/issues/kotlin/gh430/KNoInitialConditionsTest.kt b/src/test/kotlin/issues/kotlin/gh430/KNoInitialConditionsTest.kt
index 0f78c6d13..6bc03fe07 100644
--- a/src/test/kotlin/issues/kotlin/gh430/KNoInitialConditionsTest.kt
+++ b/src/test/kotlin/issues/kotlin/gh430/KNoInitialConditionsTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -175,8 +175,10 @@ class KNoInitialConditionsTest {
 
         val selectStatement = select(column1, column2) {
             from(foo)
-            where { column1 isLessThan Date() }
-            or(criteria)
+            where {
+                column1 isLessThan Date()
+                or(criteria)
+            }
         }
 
         val expected = "select column1, column2 from foo where column1 < :p1 " +
@@ -187,7 +189,9 @@ class KNoInitialConditionsTest {
     private fun buildSelectStatement(criteria: List<AndOrCriteriaGroup>) =
         select(column1, column2) {
             from(foo)
-            where { column1 isLessThan Date() }
-            and(criteria)
+            where {
+                column1 isLessThan Date()
+                and(criteria)
+            }
         }
 }
diff --git a/src/test/kotlin/nullability/test/BetweenTest.kt b/src/test/kotlin/nullability/test/BetweenTest.kt
index 51b35a49d..a6b399b69 100644
--- a/src/test/kotlin/nullability/test/BetweenTest.kt
+++ b/src/test/kotlin/nullability/test/BetweenTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/nullability/test/BetweenWhenPresentTest.kt b/src/test/kotlin/nullability/test/BetweenWhenPresentTest.kt
index 31cb1e8b2..9cf8509b0 100644
--- a/src/test/kotlin/nullability/test/BetweenWhenPresentTest.kt
+++ b/src/test/kotlin/nullability/test/BetweenWhenPresentTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/nullability/test/ComparisonTest.kt b/src/test/kotlin/nullability/test/ComparisonTest.kt
index ebd25607f..0ce1fce32 100644
--- a/src/test/kotlin/nullability/test/ComparisonTest.kt
+++ b/src/test/kotlin/nullability/test/ComparisonTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -38,7 +38,7 @@ class ComparisonTest {
         val compilerMessageCollector = compile(source)
         assertThat(compilerMessageCollector.errorLocations())
             .hasSize(1)
-// TODO            .contains(ErrorLocation(9, 30))
+            .contains(ErrorLocation(9, 20))
     }
 
     @Test
@@ -81,7 +81,7 @@ class ComparisonTest {
         val compilerMessageCollector = compile(source)
         assertThat(compilerMessageCollector.errorLocations())
             .hasSize(1)
-// TODO           .contains(ErrorLocation(10, 31))
+            .contains(ErrorLocation(10, 21))
     }
 
     @Test
@@ -124,7 +124,7 @@ class ComparisonTest {
         val compilerMessageCollector = compile(source)
         assertThat(compilerMessageCollector.errorLocations())
             .hasSize(1)
-// TODO           .contains(ErrorLocation(9, 33))
+            .contains(ErrorLocation(9, 20))
     }
 
     @Test
@@ -167,7 +167,7 @@ class ComparisonTest {
         val compilerMessageCollector = compile(source)
         assertThat(compilerMessageCollector.errorLocations())
             .hasSize(1)
-// TODO           .contains(ErrorLocation(10, 34))
+            .contains(ErrorLocation(10, 21))
     }
 
     @Test
@@ -210,7 +210,7 @@ class ComparisonTest {
         val compilerMessageCollector = compile(source)
         assertThat(compilerMessageCollector.errorLocations())
             .hasSize(1)
-// TODO           .contains(ErrorLocation(9, 34))
+            .contains(ErrorLocation(9, 20))
     }
 
     @Test
@@ -253,7 +253,7 @@ class ComparisonTest {
         val compilerMessageCollector = compile(source)
         assertThat(compilerMessageCollector.errorLocations())
             .hasSize(1)
-// TODO           .contains(ErrorLocation(10, 35))
+            .contains(ErrorLocation(10, 21))
     }
 
     @Test
@@ -296,7 +296,7 @@ class ComparisonTest {
         val compilerMessageCollector = compile(source)
         assertThat(compilerMessageCollector.errorLocations())
             .hasSize(1)
-// TODO           .contains(ErrorLocation(9, 43))
+            .contains(ErrorLocation(9, 20))
     }
 
     @Test
@@ -339,7 +339,7 @@ class ComparisonTest {
         val compilerMessageCollector = compile(source)
         assertThat(compilerMessageCollector.errorLocations())
             .hasSize(1)
-// TODO           .contains(ErrorLocation(10, 44))
+            .contains(ErrorLocation(10, 21))
     }
 
     @Test
@@ -382,7 +382,7 @@ class ComparisonTest {
         val compilerMessageCollector = compile(source)
         assertThat(compilerMessageCollector.errorLocations())
             .hasSize(1)
-// TODO           .contains(ErrorLocation(9, 31))
+            .contains(ErrorLocation(9, 20))
     }
 
     @Test
@@ -425,7 +425,7 @@ class ComparisonTest {
         val compilerMessageCollector = compile(source)
         assertThat(compilerMessageCollector.errorLocations())
             .hasSize(1)
-// TODO           .contains(ErrorLocation(10, 32))
+            .contains(ErrorLocation(10, 21))
     }
 
     @Test
@@ -468,7 +468,7 @@ class ComparisonTest {
         val compilerMessageCollector = compile(source)
         assertThat(compilerMessageCollector.errorLocations())
             .hasSize(1)
-// TODO           .contains(ErrorLocation(9, 40))
+            .contains(ErrorLocation(9, 20))
     }
 
     @Test
@@ -511,7 +511,7 @@ class ComparisonTest {
         val compilerMessageCollector = compile(source)
         assertThat(compilerMessageCollector.errorLocations())
             .hasSize(1)
-// TODO           .contains(ErrorLocation(10, 41))
+            .contains(ErrorLocation(10, 21))
     }
 
     @Test
diff --git a/src/test/kotlin/nullability/test/CompilerUtilities.kt b/src/test/kotlin/nullability/test/CompilerUtilities.kt
index 14a9d3f5a..5212de766 100644
--- a/src/test/kotlin/nullability/test/CompilerUtilities.kt
+++ b/src/test/kotlin/nullability/test/CompilerUtilities.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/nullability/test/EqualNotEqualTest.kt b/src/test/kotlin/nullability/test/EqualNotEqualTest.kt
index d1f475d69..92b5df38e 100644
--- a/src/test/kotlin/nullability/test/EqualNotEqualTest.kt
+++ b/src/test/kotlin/nullability/test/EqualNotEqualTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -38,7 +38,7 @@ class EqualNotEqualTest {
         val compilerMessageCollector = compile(source)
         assertThat(compilerMessageCollector.errorLocations())
             .hasSize(1)
-// TODO           .contains(ErrorLocation(9, 37))
+            .contains(ErrorLocation(9, 27))
     }
 
     @Test
@@ -81,7 +81,7 @@ class EqualNotEqualTest {
         val compilerMessageCollector = compile(source)
         assertThat(compilerMessageCollector.errorLocations())
             .hasSize(1)
-// TODO           .contains(ErrorLocation(10, 40))
+            .contains(ErrorLocation(10, 27))
     }
 
     @Test
@@ -124,7 +124,7 @@ class EqualNotEqualTest {
         val compilerMessageCollector = compile(source)
         assertThat(compilerMessageCollector.errorLocations())
             .hasSize(1)
-// TODO           .contains(ErrorLocation(10, 38))
+            .contains(ErrorLocation(10, 28))
     }
 
     @Test
@@ -169,7 +169,7 @@ class EqualNotEqualTest {
         val compilerMessageCollector = compile(source)
         assertThat(compilerMessageCollector.errorLocations())
             .hasSize(1)
-// TODO           .contains(ErrorLocation(11, 41))
+            .contains(ErrorLocation(11, 28))
     }
 
     @Test
diff --git a/src/test/kotlin/nullability/test/InTest.kt b/src/test/kotlin/nullability/test/InTest.kt
index 23fa9e784..c6c862373 100644
--- a/src/test/kotlin/nullability/test/InTest.kt
+++ b/src/test/kotlin/nullability/test/InTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -108,6 +108,6 @@ class InTest {
         val compilerMessageCollector = compile(source)
         assertThat(compilerMessageCollector.errorLocations())
             .hasSize(1)
-// TODO           .contains(ErrorLocation(11, 26))
+            .contains(ErrorLocation(11, 21))
     }
 }
diff --git a/src/test/kotlin/nullability/test/InWhenPresentTest.kt b/src/test/kotlin/nullability/test/InWhenPresentTest.kt
index 2d3be77b5..1ea53811c 100644
--- a/src/test/kotlin/nullability/test/InWhenPresentTest.kt
+++ b/src/test/kotlin/nullability/test/InWhenPresentTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/nullability/test/LikeNotLikeTest.kt b/src/test/kotlin/nullability/test/LikeNotLikeTest.kt
index 6fef483b3..7b7acce89 100644
--- a/src/test/kotlin/nullability/test/LikeNotLikeTest.kt
+++ b/src/test/kotlin/nullability/test/LikeNotLikeTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/nullability/test/NotBetweenTest.kt b/src/test/kotlin/nullability/test/NotBetweenTest.kt
index 7db71b41a..e45016a57 100644
--- a/src/test/kotlin/nullability/test/NotBetweenTest.kt
+++ b/src/test/kotlin/nullability/test/NotBetweenTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/nullability/test/NotBetweenWhenPresentTest.kt b/src/test/kotlin/nullability/test/NotBetweenWhenPresentTest.kt
index 6d83930c1..c3e2d7648 100644
--- a/src/test/kotlin/nullability/test/NotBetweenWhenPresentTest.kt
+++ b/src/test/kotlin/nullability/test/NotBetweenWhenPresentTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/nullability/test/NotInTest.kt b/src/test/kotlin/nullability/test/NotInTest.kt
index f88356c26..1194a7570 100644
--- a/src/test/kotlin/nullability/test/NotInTest.kt
+++ b/src/test/kotlin/nullability/test/NotInTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -108,6 +108,6 @@ class NotInTest {
         val compilerMessageCollector = compile(source)
         assertThat(compilerMessageCollector.errorLocations())
             .hasSize(1)
-// TODO           .contains(ErrorLocation(11, 29))
+            .contains(ErrorLocation(11, 21))
     }
 }
diff --git a/src/test/kotlin/nullability/test/NotInWhenPresentTest.kt b/src/test/kotlin/nullability/test/NotInWhenPresentTest.kt
index 5304b2a0b..e98ba98af 100644
--- a/src/test/kotlin/nullability/test/NotInWhenPresentTest.kt
+++ b/src/test/kotlin/nullability/test/NotInWhenPresentTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
diff --git a/src/test/kotlin/org/mybatis/dynamic/sql/util/kotlin/model/ModelBuilderTest.kt b/src/test/kotlin/org/mybatis/dynamic/sql/util/kotlin/model/ModelBuilderTest.kt
index 1d9e88675..a4a4b9e3f 100644
--- a/src/test/kotlin/org/mybatis/dynamic/sql/util/kotlin/model/ModelBuilderTest.kt
+++ b/src/test/kotlin/org/mybatis/dynamic/sql/util/kotlin/model/ModelBuilderTest.kt
@@ -1,5 +1,5 @@
 /*
- *    Copyright 2016-2024 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.
@@ -19,13 +19,14 @@ import org.assertj.core.api.Assertions.assertThat
 import org.junit.jupiter.api.Test
 import org.mybatis.dynamic.sql.SqlTable
 import org.mybatis.dynamic.sql.render.RenderingStrategies
+import org.mybatis.dynamic.sql.util.kotlin.elements.column
 
 class ModelBuilderTest {
     class Table : SqlTable("Table")
 
     val table = Table()
-    val id = table.column<Int>("id")
-    val description = table.column<String>("description")
+    val id = table.column<Int>(name = "id")
+    val description = table.column<String>(name = "description")
 
     @Test
     fun testSelectBuilder() {
@@ -46,4 +47,50 @@ class ModelBuilderTest {
 
         assertThat(provider.selectStatement).isEqualTo("select distinct id, description from Table where id = :p1")
     }
+
+    @Test
+    fun testSelectBuilderForUpdate() {
+        val provider = select(id, description) {
+            from(table)
+            where { id isEqualTo 3 }
+            forUpdate()
+            skipLocked()
+        }.render(RenderingStrategies.SPRING_NAMED_PARAMETER)
+
+        assertThat(provider.selectStatement).isEqualTo("select id, description from Table where id = :p1 for update skip locked")
+    }
+
+    @Test
+    fun testSelectBuilderForShare() {
+        val provider = select(id, description) {
+            from(table)
+            where { id isEqualTo 3 }
+            forShare()
+            nowait()
+        }.render(RenderingStrategies.SPRING_NAMED_PARAMETER)
+
+        assertThat(provider.selectStatement).isEqualTo("select id, description from Table where id = :p1 for share nowait")
+    }
+
+    @Test
+    fun testSelectBuilderForKeyShare() {
+        val provider = select(id, description) {
+            from(table)
+            where { id isEqualTo 3 }
+            forKeyShare()
+        }.render(RenderingStrategies.SPRING_NAMED_PARAMETER)
+
+        assertThat(provider.selectStatement).isEqualTo("select id, description from Table where id = :p1 for key share")
+    }
+
+    @Test
+    fun testSelectBuilderForKeyNoKeyUpdate() {
+        val provider = select(id, description) {
+            from(table)
+            where { id isEqualTo 3 }
+            forNoKeyUpdate()
+        }.render(RenderingStrategies.SPRING_NAMED_PARAMETER)
+
+        assertThat(provider.selectStatement).isEqualTo("select id, description from Table where id = :p1 for no key update")
+    }
 }
diff --git a/src/test/resources/defaultTrue.properties b/src/test/resources/defaultTrue.properties
index 95959bf37..bca00c4b3 100644
--- a/src/test/resources/defaultTrue.properties
+++ b/src/test/resources/defaultTrue.properties
@@ -1,5 +1,5 @@
 #
-#    Copyright 2016-2024 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.
diff --git a/src/test/resources/empty.properties b/src/test/resources/empty.properties
index 8694a62c2..c3201cfb7 100644
--- a/src/test/resources/empty.properties
+++ b/src/test/resources/empty.properties
@@ -1,5 +1,5 @@
 #
-#    Copyright 2016-2024 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.
diff --git a/src/test/resources/examples/animal/data/CreateAnimalData.sql b/src/test/resources/examples/animal/data/CreateAnimalData.sql
index cb19f64b1..4443da73a 100644
--- a/src/test/resources/examples/animal/data/CreateAnimalData.sql
+++ b/src/test/resources/examples/animal/data/CreateAnimalData.sql
@@ -1,5 +1,5 @@
 --
---    Copyright 2016-2024 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.
diff --git a/src/test/resources/examples/array/CreateDB.sql b/src/test/resources/examples/array/CreateDB.sql
index d03a4aadc..fd7fa4b04 100644
--- a/src/test/resources/examples/array/CreateDB.sql
+++ b/src/test/resources/examples/array/CreateDB.sql
@@ -1,5 +1,5 @@
 --
---    Copyright 2016-2024 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.
diff --git a/src/test/resources/examples/column/comparison/CreateDB.sql b/src/test/resources/examples/column/comparison/CreateDB.sql
index 9b414e4ea..80f8d6b21 100644
--- a/src/test/resources/examples/column/comparison/CreateDB.sql
+++ b/src/test/resources/examples/column/comparison/CreateDB.sql
@@ -1,5 +1,5 @@
 --
---    Copyright 2016-2024 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.
diff --git a/src/test/resources/examples/custom_render/dbInit.sql b/src/test/resources/examples/custom_render/dbInit.sql
index defb7f5f6..af5d17495 100644
--- a/src/test/resources/examples/custom_render/dbInit.sql
+++ b/src/test/resources/examples/custom_render/dbInit.sql
@@ -1,5 +1,5 @@
 --
---    Copyright 2016-2024 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.
diff --git a/src/test/resources/examples/generated/always/CreateGeneratedAlwaysDB.sql b/src/test/resources/examples/generated/always/CreateGeneratedAlwaysDB.sql
index c7038b187..9bd7ad450 100644
--- a/src/test/resources/examples/generated/always/CreateGeneratedAlwaysDB.sql
+++ b/src/test/resources/examples/generated/always/CreateGeneratedAlwaysDB.sql
@@ -1,5 +1,5 @@
 --
---    Copyright 2016-2024 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.
diff --git a/src/test/resources/examples/groupby/CreateGroupByDB.sql b/src/test/resources/examples/groupby/CreateGroupByDB.sql
index 8f5eb0c33..427c514c7 100644
--- a/src/test/resources/examples/groupby/CreateGroupByDB.sql
+++ b/src/test/resources/examples/groupby/CreateGroupByDB.sql
@@ -1,5 +1,5 @@
 --
---    Copyright 2016-2024 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.
diff --git a/src/test/resources/examples/joins/CreateJoinDB.sql b/src/test/resources/examples/joins/CreateJoinDB.sql
index 37ed62d87..675fb9dcc 100644
--- a/src/test/resources/examples/joins/CreateJoinDB.sql
+++ b/src/test/resources/examples/joins/CreateJoinDB.sql
@@ -1,5 +1,5 @@
 --
---    Copyright 2016-2024 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.
diff --git a/src/test/resources/examples/joins/JoinMapper.xml b/src/test/resources/examples/joins/JoinMapper.xml
index ba6ff8c7a..22aa930e9 100644
--- a/src/test/resources/examples/joins/JoinMapper.xml
+++ b/src/test/resources/examples/joins/JoinMapper.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
 
-       Copyright 2016-2024 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.
@@ -22,10 +22,12 @@
     <id column="order_id" jdbcType="INTEGER" property="id" />
     <result column="order_date" jdbcType="DATE" property="orderDate" />
     <collection property="details" ofType="examples.joins.OrderDetail">
-      <id column="order_id" jdbcType="INTEGER" property="orderId"/>
-      <id column="line_number" jdbcType="INTEGER" property="lineNumber"/>
-      <result column="description" jdbcType="VARCHAR" property="description"/>
-      <result column="quantity" jdbcType="INTEGER" property="quantity"/>
+      <constructor>
+        <idArg column="order_id" jdbcType="INTEGER" javaType="Integer" />
+        <idArg column="line_number" jdbcType="INTEGER" javaType="Integer" />
+        <arg column="description" jdbcType="VARCHAR" javaType="String" />
+        <arg column="quantity" jdbcType="INTEGER" javaType="Integer" />
+      </constructor>
     </collection>
   </resultMap>
 </mapper>
diff --git a/src/test/resources/examples/kotlin/mybatis3/CreateSimpleDB.sql b/src/test/resources/examples/kotlin/mybatis3/CreateSimpleDB.sql
index 11c516de2..769a0fed4 100644
--- a/src/test/resources/examples/kotlin/mybatis3/CreateSimpleDB.sql
+++ b/src/test/resources/examples/kotlin/mybatis3/CreateSimpleDB.sql
@@ -1,5 +1,5 @@
 --
---    Copyright 2016-2024 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.
diff --git a/src/test/resources/examples/kotlin/mybatis3/joins/CreateJoinDB.sql b/src/test/resources/examples/kotlin/mybatis3/joins/CreateJoinDB.sql
index 5400a658a..8b14e95c0 100644
--- a/src/test/resources/examples/kotlin/mybatis3/joins/CreateJoinDB.sql
+++ b/src/test/resources/examples/kotlin/mybatis3/joins/CreateJoinDB.sql
@@ -1,5 +1,5 @@
 --
---    Copyright 2016-2024 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.
diff --git a/src/test/resources/examples/kotlin/mybatis3/joins/JoinMapper.xml b/src/test/resources/examples/kotlin/mybatis3/joins/JoinMapper.xml
index 7f9aef6cc..bb653837f 100644
--- a/src/test/resources/examples/kotlin/mybatis3/joins/JoinMapper.xml
+++ b/src/test/resources/examples/kotlin/mybatis3/joins/JoinMapper.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
 
-       Copyright 2016-2024 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.
diff --git a/src/test/resources/examples/kotlin/spring/CreateGeneratedAlwaysDB.sql b/src/test/resources/examples/kotlin/spring/CreateGeneratedAlwaysDB.sql
index 24304f983..ef17b67b4 100644
--- a/src/test/resources/examples/kotlin/spring/CreateGeneratedAlwaysDB.sql
+++ b/src/test/resources/examples/kotlin/spring/CreateGeneratedAlwaysDB.sql
@@ -1,5 +1,5 @@
 --
---    Copyright 2016-2024 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.
diff --git a/src/test/resources/examples/kotlin/spring/CreateSimpleDB.sql b/src/test/resources/examples/kotlin/spring/CreateSimpleDB.sql
index 38940e55e..0527b19bb 100644
--- a/src/test/resources/examples/kotlin/spring/CreateSimpleDB.sql
+++ b/src/test/resources/examples/kotlin/spring/CreateSimpleDB.sql
@@ -1,5 +1,5 @@
 --
---    Copyright 2016-2024 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.
diff --git a/src/test/resources/examples/mariadb/CreateDB.sql b/src/test/resources/examples/mariadb/CreateDB.sql
index 3e16f6a73..8ea4bab28 100644
--- a/src/test/resources/examples/mariadb/CreateDB.sql
+++ b/src/test/resources/examples/mariadb/CreateDB.sql
@@ -1,5 +1,5 @@
 --
---    Copyright 2016-2024 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.
@@ -41,3 +41,15 @@ insert into items values (17, 'Item 17', 117);
 insert into items values (18, 'Item 18', 118);
 insert into items values (19, 'Item 19', 119);
 insert into items values (20, 'Item 20', 120);
+
+create table numbers (
+    id int not null,
+    description varchar(50) not null,
+    primary key (id)
+);
+
+insert into numbers values (1, 'One');
+insert into numbers values (2, 'Two');
+insert into numbers values (3, 'Three');
+insert into numbers values (4, 'Four');
+insert into numbers values (5, 'Five');
diff --git a/src/test/resources/examples/postgres/dbInit.sql b/src/test/resources/examples/postgres/dbInit.sql
new file mode 100644
index 000000000..03ba34cac
--- /dev/null
+++ b/src/test/resources/examples/postgres/dbInit.sql
@@ -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.
+--
+
+create table TableCode (
+  id int not null,
+  description varchar(30) not null,
+  primary key (id)
+);
+
+insert into TableCode (id, description) values(1, 'One');
+insert into TableCode (id, description) values(2, 'Two');
+insert into TableCode (id, description) values(3, 'Three');
+insert into TableCode (id, description) values(4, 'Four');
diff --git a/src/test/resources/examples/schema_supplier/CreateDB.sql b/src/test/resources/examples/schema_supplier/CreateDB.sql
index 7d9d57f8b..2a9f46f47 100644
--- a/src/test/resources/examples/schema_supplier/CreateDB.sql
+++ b/src/test/resources/examples/schema_supplier/CreateDB.sql
@@ -1,5 +1,5 @@
 --
---    Copyright 2016-2024 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.
diff --git a/src/test/resources/examples/sharding/ShardingDB.sql b/src/test/resources/examples/sharding/ShardingDB.sql
index 5af3d717b..e46a0403a 100644
--- a/src/test/resources/examples/sharding/ShardingDB.sql
+++ b/src/test/resources/examples/sharding/ShardingDB.sql
@@ -1,5 +1,5 @@
 --
---    Copyright 2016-2024 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.
diff --git a/src/test/resources/examples/simple/CreateSimpleDB.sql b/src/test/resources/examples/simple/CreateSimpleDB.sql
index e750bb6f0..c335c4215 100644
--- a/src/test/resources/examples/simple/CreateSimpleDB.sql
+++ b/src/test/resources/examples/simple/CreateSimpleDB.sql
@@ -1,5 +1,5 @@
 --
---    Copyright 2016-2024 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.
@@ -30,7 +30,7 @@ create table Address (
 create table Person (
    id int not null,
    first_name varchar(30) not null,
-   last_name varchar(30) not null,
+   last_name varchar(30) null,
    birth_date date not null,
    employed varchar(3) not null,
    occupation varchar(30) null,
diff --git a/src/test/resources/examples/springbatch/data.sql b/src/test/resources/examples/springbatch/data.sql
index 08aff9937..81cd366b2 100644
--- a/src/test/resources/examples/springbatch/data.sql
+++ b/src/test/resources/examples/springbatch/data.sql
@@ -1,5 +1,5 @@
 --
---    Copyright 2016-2024 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.
diff --git a/src/test/resources/examples/springbatch/schema.sql b/src/test/resources/examples/springbatch/schema.sql
index 3d02988bc..63fdfd66a 100644
--- a/src/test/resources/examples/springbatch/schema.sql
+++ b/src/test/resources/examples/springbatch/schema.sql
@@ -1,5 +1,5 @@
 --
---    Copyright 2016-2024 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.
diff --git a/src/test/resources/examples/type_conversion/CreateDB.sql b/src/test/resources/examples/type_conversion/CreateDB.sql
index a8626e6a8..61cbdb580 100644
--- a/src/test/resources/examples/type_conversion/CreateDB.sql
+++ b/src/test/resources/examples/type_conversion/CreateDB.sql
@@ -1,5 +1,5 @@
 --
---    Copyright 2016-2024 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.
diff --git a/src/test/resources/issues/gh324/CreateDB.sql b/src/test/resources/issues/gh324/CreateDB.sql
index 84e459f70..77bb85949 100644
--- a/src/test/resources/issues/gh324/CreateDB.sql
+++ b/src/test/resources/issues/gh324/CreateDB.sql
@@ -1,5 +1,5 @@
 --
---    Copyright 2016-2024 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.
diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml
index f760920af..0f6730638 100644
--- a/src/test/resources/logback.xml
+++ b/src/test/resources/logback.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
 
-       Copyright 2016-2024 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.
diff --git a/src/test/resources/mybatis-dynamic-sql.properties b/src/test/resources/mybatis-dynamic-sql.properties
index 8f2b0d2de..fc8f38834 100644
--- a/src/test/resources/mybatis-dynamic-sql.properties
+++ b/src/test/resources/mybatis-dynamic-sql.properties
@@ -1,5 +1,5 @@
 #
-#    Copyright 2016-2024 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.