You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iotdb.apache.org by ha...@apache.org on 2026/10/09 09:19:32 UTC

(iotdb-extras) branch master updated: feat!: align integrations with IoTDB 2.0.11 and JDK 17 (#132)

This is an automated email from the ASF dual-hosted git repository.

HTHou pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb-extras.git


The following commit(s) were added to refs/heads/master by this push:
     new 404a4bf  feat!: align integrations with IoTDB 2.0.11 and JDK 17 (#132)
404a4bf is described below

commit 404a4bff2b5903cfdd845be86732015fc72a9bda
Author: CritasWang <cr...@outlook.com>
AuthorDate: Fri Oct 9 17:19:26 2026 +0800

    feat!: align integrations with IoTDB 2.0.11 and JDK 17 (#132)
    
    * feat!: align integrations with IoTDB 2.0.11 and JDK 17
    
    Align client dependencies and connector APIs with the released IoTDB
    2.0.11 baseline. Add Spring Boot 4 compatibility and audit starter pool
    configuration, including the default fetch size of 5000.
    
    Add MyBatis query and type adapters, bounded batch generation, nullable
    composite keys, and IoTDB-specific MyBatis-Plus operations and templates.
    Update English documentation and the existing Chinese counterparts.
    
    Validation: the full JDK 17 reactor, MyBatis offline and live ms suites,
    and Spring Boot 3/4 compatibility checks on JDK 17/21 passed. Document
    the upstream us/ns time-filtered DELETE issue without weakening tests.
    
    BREAKING CHANGE: Java modules require JDK 17. Regenerate MyBatis mapper
    interfaces and XML together; TIME/TIMESTAMP fields use Long, and the
    MyBatis-Plus example exposes only IoTDB-supported mapper operations.
    
    * ci: make the MyBatis integration job reach the IoTDB service
    
    The service container only listened on its loopback because the image
    defaults dn_rpc_address to 127.0.0.1, so the published port accepted the
    TCP handshake and the JDBC session was reset. Bind it to all interfaces,
    as the Testcontainers ITs already do, and replace the TCP probe with a
    real client round trip through start-cli: the port is reachable several
    seconds before the DataNode can open a session.
    
    * build: run the reactor quality gates on the Spring Boot examples
    
    The MyBatis-Plus example had no parent since the Spring Boot starter
    parent was dropped, so Spotless ran from a hand-copied configuration and
    Checkstyle, RAT and the reactor's plugin management never touched it.
    Inherit the examples parent and re-point the inherited Spring, Jackson
    and Mockito management at the selected Boot line for both the default
    Boot 3.5.1 build and the spring-boot4 profile; surefire moves to a
    JUnit Platform 6 capable version for Boot 4.
    
    The Spring Boot starter example now uses the reactor's google-java-format
    1.28.0 / Spotless 2.44.5 instead of the pins the reactor moved away from.
    
    * mybatis: drop unsupported UPDATE statements and ship the runtime jar
    
    IoTDB 2.0.11 UPDATE rejects time in its predicate and FIELD columns in
    SET, so MBG's key-based UPDATE statements can never run. IoTDBKeyPlugin
    now disables them with a generator warning instead of emitting SQL that
    fails at runtime, and gains unit coverage for the nullable-TAG predicate
    rewrite it already performed.
    
    The mybatis-support runtime jar is required by every generated mapper
    but was only reachable through mvn install; the new with-mybatis
    distribution profile packages it next to the generator plugin.
    
    * collector: fail task creation when the IoTDB source cannot consume
    
    The IoTDB push source opened its subscription on the worker thread, so a
    missing topic, a tsfile-format topic, an unreachable broker or a server
    without subscription support only produced one log line while the task
    stayed registered and silently never delivered. Validate the topic
    format and subscribe on the calling thread so start() reports the cause,
    and let PushSourceTask/PullSourceTask propagate a start failure even
    when closing the source succeeds, matching SinkTask and ProcessorTask.
    
    * thingsboard: drop per-test databases and refresh 2.0.11 notes
    
    The container ITs provisioned a fresh database per test and never
    dropped it; on an 8 GB Docker host the DataNode ran out of region memory
    after about a dozen databases and later tests timed out on their first
    write. Track the databases each test creates and drop them afterwards.
    
    apache/iotdb#18300 (grouped MAX of non-positive floats) ships in 2.0.11,
    so the DAO comment no longer claims it is unreleased; the -MIN(-x)
    projection stays for older servers. Remaining 2.0.8 references in the
    README and migration guide now name the verified 2.0.11 behavior.
    
    * chore: document the starter timeout default and drop unused properties
    
    The starter README notes that connection-timeout-in-ms now defaults to 0
    instead of failing startup with a NullPointerException when unset. The
    compile.version property in the Flink and Hive connector poms had no
    consumer.
    
    * collector: release every stage when task creation fails
    
    Task creation now fails when a source cannot start, so the task is never
    registered and nothing drops it later. Sources started before the one
    that failed kept their subscriptions, pull loops kept polling, and the
    sink and processor kept their workers; their executors stayed in the
    per-task-id registry, so a retry under the same id reused executors
    whose threads still ran the failed attempt's workers and delivered
    nothing. TaskCombiner.create() now drops all three stages on failure,
    and drop is the single cleanup path: the per-plugin close in each
    createInternal is gone and dropInternal skips slots that were never
    constructed.
    
    Stopping a disruptor needed more than one halt: Disruptor 3.x discards a
    halt that reaches a worker before it enters run(), and that worker then
    parks forever, which a quick creation failure hits reliably. Sink and
    processor stages now drain their backlog for a bounded time and halt
    until their executor has terminated, before closing their plugins; the
    pull source waits for its loops before closing its sources. A normal
    drop marks every stage dropped before draining any, because a paused
    sink did not consume what the processor drained and each drain waited
    for its timeout.
    
    The IoTDB source looks up the topic through SubscriptionTreeSessionBuilder
    instead of the deprecated SubscriptionTreeSession constructor.
    
    * mybatis: map FLOAT to Float in the default generator config
    
    The generator's own generatorConfig.xml still generated Double fields for
    FLOAT columns while the README and the example configurations use the
    new Float contract. The plugin tests parse mappers through the
    InputStream XMLMapperBuilder constructor; the Reader one is deprecated.
    
    * Potential fix for pull request finding 'CodeQL / Confusing overloading of methods'
    
    Co-authored-by: Copilot Autofix powered by AI <62...@users.noreply.github.com>
    
    * Potential fix for pull request finding 'CodeQL / Confusing overloading of methods'
    
    Co-authored-by: Copilot Autofix powered by AI <62...@users.noreply.github.com>
    
    * collector: restore the PipeConnector overload in TaskCombinerTest
    
    The CodeQL autofix for java/confusing-method-signature deleted
    FakeSink.customize(PipeParameters, PipeConnectorRuntimeConfiguration),
    but PipeSink leaves that PipeConnector overload abstract, so the
    collector-core tests no longer compiled and every Java job of the PR
    failed. The overload is back. The PipeSinkRuntimeConfiguration
    overload forwards to it and the tablet and TsFile transfer overloads
    forward to transfer(Event), each through an upcast, which is the
    delegation the query accepts; transfer(TsFileInsertionEvent) is
    overridden only for that reason. The forwarding transfer overloads
    declare InterruptedException instead of wrapping it in a
    RuntimeException.
    
    ---------
    
    Co-authored-by: Copilot Autofix powered by AI <62...@users.noreply.github.com>
---
 .github/workflows/compile-check.yml                |  61 ++-
 Jenkinsfile                                        |   2 +-
 README-zh.md                                       |  31 +-
 README.md                                          |  31 +-
 connectors/flink-iotdb-connector/pom.xml           |   1 -
 .../iotdb/flink/tsfile/util/TSFileConfigUtil.java  |   1 +
 .../util/TSFileConfigUtilCompletenessTest.java     |   3 +-
 connectors/grafana-connector/pom.xml               |  16 +
 connectors/grafana-plugin/docker-compose.yaml      |   2 +-
 connectors/hive-connector/pom.xml                  |   1 -
 .../java/org/apache/iotdb/hive/TsFileSerDe.java    |   2 +-
 connectors/spark-iotdb-connector/pom.xml           |   4 +-
 connectors/spark-iotdb-table-connector/pom.xml     |   4 +-
 distributions/pom.xml                              |  70 ++++
 .../src/assembly/mybatis-generator-plugin.xml      |   8 +
 examples/iotdb-spring-boot-start/README.md         | 105 +-----
 examples/iotdb-spring-boot-start/pom.xml           |   8 +-
 .../service/IoTDBService.java                      |  39 +-
 .../src/main/resources/application.properties      |   5 +-
 examples/kafka/readme.md                           |   6 +-
 examples/mybatis-generator/README.md               | 126 +++----
 examples/mybatis-generator/pom.xml                 |  70 +++-
 .../main/java/org/apache/iotdb/mybatis/Main.java   |  43 ++-
 .../iotdb/mybatis/plugin/mapper/MixMapper.java     |  21 +-
 .../org/apache/iotdb/mybatis/plugin/model/Mix.java |  14 +-
 .../apache/iotdb/mybatis/plugin/xml/MixMapper.xml  |  71 ++--
 .../src/main/resources/generatorConfig.xml         |  13 +-
 .../main/resources/generatorConfigByExample.xml    |  15 +-
 .../src/main/resources/mybatis-config.xml          |   3 +
 .../org/apache/iotdb/mybatis/IoTDBGeneratorIT.java | 317 ++++++++++++++++
 .../iotdb/mybatis/MapperConfigurationTest.java     |  65 ++++
 examples/mybatisplus-generator/README.md           | 198 +++-------
 examples/mybatisplus-generator/pom.xml             | 150 +++++---
 .../main/java/org/apache/iotdb/CodeGenerator.java  | 351 +++++++++++++++++
 .../src/main/java/org/apache/iotdb/Main.java       |  86 +----
 .../iotdb/config/IoTDBMybatisConfiguration.java    |  53 +++
 .../main/java/org/apache/iotdb/entity/Table1.java  |  83 ++--
 .../main/java/org/apache/iotdb/entity/Table2.java  |  83 ++--
 .../org/apache/iotdb/mapper/IoTDBTableMapper.java  |  40 ++
 .../java/org/apache/iotdb/mapper/Table1Mapper.java |  37 +-
 .../java/org/apache/iotdb/mapper/Table2Mapper.java |  37 +-
 .../org/apache/iotdb/service/Table1Service.java    |  48 +--
 .../org/apache/iotdb/service/Table2Service.java    |  48 +--
 .../iotdb/service/impl/Table1ServiceImpl.java      |  98 +++--
 .../iotdb/service/impl/Table2ServiceImpl.java      |  98 +++--
 .../java/org/apache/iotdb/xml/Table1Mapper.xml     |  23 --
 .../java/org/apache/iotdb/xml/Table2Mapper.xml     |  23 --
 .../src/main/resources/application.yml             |   3 +
 .../src/main/resources/mappers/Table1Mapper.xml    |  88 +++++
 .../src/main/resources/mappers/Table2Mapper.xml    |  88 +++++
 .../main/resources/templates/iotdb-entity.java.vm  |  24 ++
 .../main/resources/templates/iotdb-mapper.java.vm  |  32 ++
 .../main/resources/templates/iotdb-mapper.xml.vm   |  41 ++
 .../java/org/apache/iotdb/ApplicationTest.java     | 164 ++++++--
 .../java/org/apache/iotdb/CodeGeneratorTest.java   | 123 ++++++
 .../test/java/org/apache/iotdb/IoTDBMapperIT.java  | 273 ++++++++++++++
 examples/pom.xml                                   |   4 +-
 examples/rabbitmq/readme.md                        |   5 +-
 examples/rocketmq/readme.md                        |   4 +-
 iotdb-collector/collector-core/pom.xml             |  17 +-
 .../sink/event/PipeRawTabletInsertionEvent.java    |   8 +
 .../builtin/source/iotdb/IoTDBPushSource.java      |  96 ++++-
 .../apache/iotdb/collector/runtime/task/Task.java  |  74 ++++
 .../iotdb/collector/runtime/task/TaskCombiner.java |  26 +-
 .../runtime/task/processor/ProcessorTask.java      |  40 +-
 .../collector/runtime/task/sink/SinkTask.java      |  38 +-
 .../runtime/task/source/pull/PullSourceTask.java   |  42 +--
 .../runtime/task/source/push/PushSourceTask.java   |  29 +-
 .../collector/runtime/task/TaskCombinerTest.java   | 417 +++++++++++++++++++++
 iotdb-spring-boot-starter/README.md                | 169 +++++++--
 iotdb-spring-boot-starter/pom.xml                  |  37 +-
 .../iotdb/config/IoTDBSessionProperties.java       |  67 +++-
 .../org/apache/iotdb/session/IoTDBSessionPool.java | 133 +++----
 .../additional-spring-configuration-metadata.json  |  20 +
 .../apache/iotdb/session/IoTDBSessionPoolTest.java | 348 +++++++++++++++++
 iotdb-thingsboard-table/CI-NOTES.md                |  22 +-
 iotdb-thingsboard-table/README.md                  |  17 +-
 iotdb-thingsboard-table/docker-compose.bench.yml   |   4 +-
 iotdb-thingsboard-table/docker-compose.test.yml    |   2 +-
 iotdb-thingsboard-table/docs/benchmarks/README.md  |   4 +-
 iotdb-thingsboard-table/docs/migration-guide.md    |  19 +-
 iotdb-thingsboard-table/docs/user-guide.md         |   4 +-
 iotdb-thingsboard-table/pom.xml                    |  14 +-
 .../thingsboard/table/IoTDBTableConfiguration.java |   2 +-
 .../thingsboard/table/IoTDBTableTimeseriesDao.java |  74 ++--
 .../table/IoTDBTableAttributesDaoIT.java           |  24 +-
 .../table/IoTDBTableIngestionBenchmarkIT.java      |  26 +-
 .../thingsboard/table/IoTDBTableLatestDaoIT.java   |  24 +-
 .../table/IoTDBTableTimeseriesAggregationIT.java   |  28 +-
 .../table/IoTDBTableTimeseriesDaoIT.java           |  28 +-
 .../table/IoTDBTableTimeseriesDaoTest.java         |   6 +-
 .../extras/thingsboard/table/IoTDBTableTtlIT.java  |  29 +-
 mybatis-generator/README-zh.md                     |  92 +++--
 mybatis-generator/README.md                        | 114 ++++--
 mybatis-generator/pom.xml                          |  11 +
 .../iotdb/mybatis/plugin/BatchInsertPlugin.java    | 138 ++++---
 .../iotdb/mybatis/plugin/IoTDBKeyPlugin.java       | 112 ++++++
 .../apache/iotdb/mybatis/plugin/LombokPlugin.java  |  12 +
 .../src/main/resources/generatorConfig.xml         |   7 +-
 .../mybatis/plugin/BatchInsertPluginTest.java      | 274 ++++++++++++++
 .../iotdb/mybatis/plugin/IoTDBKeyPluginTest.java   | 184 +++++++++
 mybatis-support/README.md                          | 102 +++++
 mybatis-support/pom.xml                            |  40 ++
 .../iotdb/mybatis/IoTDBQueryInterceptor.java       |  84 +++++
 .../iotdb/mybatis/type/IoTDBBlobTypeHandler.java   |  57 +++
 .../mybatis/type/IoTDBLocalDateTypeHandler.java    |  62 +++
 .../iotdb/mybatis/IoTDBQueryInterceptorTest.java   |  86 +++++
 .../iotdb/mybatis/type/IoTDBTypeHandlerTest.java   | 156 ++++++++
 pom.xml                                            | 216 ++++++-----
 109 files changed, 5720 insertions(+), 1378 deletions(-)

diff --git a/.github/workflows/compile-check.yml b/.github/workflows/compile-check.yml
index 801631c..71ca659 100644
--- a/.github/workflows/compile-check.yml
+++ b/.github/workflows/compile-check.yml
@@ -1,4 +1,4 @@
-#  This workflow will compile IoTDB under jdk8 to check for compatibility issues
+# Build Java integrations on the minimum supported JDK and the next LTS.
 
 name: Compile Check
 
@@ -34,7 +34,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        java: [8, 11, 17, 21]
+        java: [17, 21]
         os: [ ubuntu-latest ]
     runs-on: ${{ matrix.os }}
     timeout-minutes: 30
@@ -47,9 +47,56 @@ jobs:
           java-version: ${{ matrix.java }}
       - name: Compiler Test
         shell: bash
+        run: mvn clean verify -Pwith-springboot,with-all-connectors,with-examples,with-thingsboard,with-grafana-connector -ntp
+      - name: Spring Boot 4 Starter Test
+        shell: bash
+        run: mvn clean test -Pwith-springboot,spring-boot4 -pl iotdb-spring-boot-starter -am -ntp
+      - name: Spring Boot 4 MyBatis-Plus Test
+        shell: bash
+        run: mvn clean test -Pwith-examples,with-springboot,spring-boot4 -pl examples/mybatisplus-generator -am -ntp
+
+  mybatis-integration:
+    runs-on: ubuntu-latest
+    timeout-minutes: 20
+    strategy:
+      fail-fast: false
+      matrix:
+        profiles: [iotdb-mybatis-it, 'iotdb-mybatis-it,spring-boot4']
+    services:
+      iotdb:
+        image: apache/iotdb:2.0.11-standalone
+        env:
+          MEMORY_SIZE: 1G
+          # The image binds the client RPC service to 127.0.0.1 by default, which the
+          # published port cannot reach; bind to all interfaces like the Testcontainers ITs do.
+          dn_rpc_address: 0.0.0.0
+        ports:
+          - 6667:6667
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-java@v4
+        with:
+          distribution: liberica
+          java-version: 17
+          cache: maven
+      - name: Wait for IoTDB to accept client sessions
+        # A TCP probe passes as soon as the port is published, several seconds before the
+        # DataNode can open a session, so wait for a real client round trip instead.
         run: |
-          if [ "${{ matrix.java }}" -ge 17 ]; then
-            mvn clean verify -P with-springboot -P with-all-connectors -P with-examples -P with-thingsboard -ntp
-          else
-            mvn clean verify -P with-all-connectors -P with-examples -ntp
-          fi
+          for attempt in $(seq 1 120); do
+            if docker exec "${{ job.services.iotdb.id }}" timeout 15 /iotdb/sbin/start-cli.sh \
+                -h 127.0.0.1 -p 6667 -u root -pw root -e 'SHOW VERSION' >/dev/null 2>&1; then
+              echo "IoTDB accepted a client session after ${attempt} attempt(s)"
+              exit 0
+            fi
+            sleep 2
+          done
+          echo 'IoTDB did not accept a client session within 4 minutes' >&2
+          docker logs "${{ job.services.iotdb.id }}" 2>&1 | tail -n 100
+          exit 1
+      - name: Generate, compile and execute MyBatis mappers
+        run: >-
+          mvn verify -Pwith-examples,with-springboot,${{ matrix.profiles }}
+          -pl examples/mybatis-generator,examples/mybatisplus-generator -am
+          '-Diotdb.it.url=jdbc:iotdb://127.0.0.1:6667/?sql_dialect=table'
+          -Diotdb.it.precision=ms
diff --git a/Jenkinsfile b/Jenkinsfile
index f9dd776..da14cc4 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -34,7 +34,7 @@ pipeline {
 
     tools {
         maven 'maven_3_latest'
-        jdk 'jdk_11_latest'
+        jdk 'jdk_17_latest'
     }
 
     options {
diff --git a/README-zh.md b/README-zh.md
index ffe425f..3616075 100644
--- a/README-zh.md
+++ b/README-zh.md
@@ -65,9 +65,13 @@ IoTDB (物联网数据库) 是一个专为物联网 (IoT) 场景设计的时序
 
 7. **示例**:展示 IoTDB 与各种技术结合使用的示例应用程序和代码示例
 
+IoTDB Java 依赖统一为 **2.0.11**,配套 **TsFile 2.4.0**、**Thrift 0.23.0**。Extras 自身版本仍为 `2.0.4-SNAPSHOT`,依赖基准为主仓库已发布的 `v2.0.11` 标签。
+
+JDK 17 是最低要求,JDK 21 也纳入 CI。外部引擎需要单独确认对所选 JDK 的支持情况,尤其是 Spark 2.4/Scala 2.11 模块,部署到 JDK 17 前仍需完成运行时迁移。
+
 ## 环境要求
 
-- Java 8+ (JDK 1.8 或更高版本,springboot 需要 JDK 17+,推荐使用 JDK 17+)
+- JDK 17+:所有 Java 模块的构建和运行基线,CI 覆盖 JDK 17 和 21
 - Maven 3.6 或更高版本
 - Git
 
@@ -85,7 +89,7 @@ IoTDB (物联网数据库) 是一个专为物联网 (IoT) 场景设计的时序
 2. 使用 Maven 构建项目:
 
    ```bash
-   # 构建整个项目(包括 distributions、iotdb-collector、metric-scrape、mybatis-generator)
+   # 构建整个项目(包括 distributions、iotdb-collector、metric-scrape、mybatis-generator、mybatis-support)
    mvn clean package -DskipTests
 
    # 或者构建所有组件
@@ -106,6 +110,12 @@ IoTDB-Extras 使用 Maven profiles 配置不同的构建选项。您可以组合
   mvn clean package -Pwith-springboot -DskipTests
   ```
 
+  Starter 默认使用 Spring Boot 3.5.1。使用下面的命令验证同一个 starter 在 Spring Boot 4.1.1 下的兼容性:
+
+  ```bash
+  mvn clean test -Pwith-springboot,spring-boot4 -pl iotdb-spring-boot-starter -am
+  ```
+
 - **with-examples**:构建示例应用程序(建议与连接器 Profiles 一起使用)
 
   ```bash
@@ -132,6 +142,12 @@ IoTDB-Extras 使用 Maven profiles 配置不同的构建选项。您可以组合
   mvn clean package -Pwith-flink -DskipTests
   ```
 
+- **with-mybatis**:将 MyBatis 生成插件和运行时适配打包为分发 zip
+
+  ```bash
+  mvn clean package -Pwith-mybatis -DskipTests
+  ```
+
 - **with-grafana**:构建 Grafana 连接器和插件
 
   ```bash
@@ -266,8 +282,9 @@ mvn clean package -Pwith-all-connectors,with-examples,with-springboot -DskipTest
 
 - **简介**:展示如何使用 iotdb-spring-boot-starter
 - **版本**:
-  - IoTDB: 2.0.3
-  - iotdb-spring-boot-starter: 2.0.3
+  - IoTDB: 2.0.11
+  - iotdb-spring-boot-starter: 2.0.4-SNAPSHOT
+  - Starter 构建线:默认 Spring Boot 3.5.1;CI 已验证 Spring Boot 4.1.1
 - **设置**:
   1. 安装 IoTDB
   2. 创建必要的数据库和表
@@ -292,8 +309,8 @@ mvn clean package -Pwith-all-connectors,with-examples,with-springboot -DskipTest
 
 - **简介**:展示如何使用 IoTDB-Mybatis-Generator
 - **版本**:
-  - IoTDB: 2.0.2
-  - mybatis-generator-plugin: 1.3.2
+  - IoTDB: 2.0.11
+  - mybatis-generator-plugin: 2.0.4-SNAPSHOT
 - **设置**:
   1. 安装并启动 IoTDB
   2. 创建必要的数据库和表
@@ -304,6 +321,8 @@ mvn clean package -Pwith-all-connectors,with-examples,with-springboot -DskipTest
 - [Flink 示例](/examples/flink/README.md)
 - [Spark 表示例](/examples/spark-table/README.md)
 - [MyBatis 生成器示例](/examples/mybatis-generator/README.md)
+- [MyBatis 运行时查询与类型适配](/mybatis-support/README.md)
+- [MyBatis-Plus / Spring Boot 3、4 示例](/examples/mybatisplus-generator/README.md)
 - [IoTDB Spring Boot Starter 示例](/examples/iotdb-spring-boot-start/README.md)
 - [Kafka 示例](/examples/kafka/readme.md)
 
diff --git a/README.md b/README.md
index 270ead1..3cc7831 100644
--- a/README.md
+++ b/README.md
@@ -67,10 +67,14 @@ This repository includes:
 
 ## Prerequisites
 
-- Java 8+ (JDK 1.8 or later versions, springboot requires JDK 17+, Recommended JDK 17+)
+- JDK 17+ for building and running Java modules; CI covers JDK 17 and 21
 - Maven 3.6 or later
 - Git
 
+IoTDB Java dependencies use **2.0.11**, paired with **TsFile 2.4.0** and **Thrift 0.23.0**. Extras keeps its own artifact version (`2.0.4-SNAPSHOT`). The dependency baseline is the released IoTDB `v2.0.11` tag, not unreleased `master` snapshots.
+
+JDK 17 is the minimum supported version; CI also covers JDK 21. External engines must support the chosen JDK independently. In particular, the legacy Spark 2.4/Scala 2.11 modules still require a runtime migration before deployment on JDK 17.
+
 ## Building from Source
 
 To build the project from source, follow these steps:
@@ -85,7 +89,7 @@ To build the project from source, follow these steps:
 2. Build the project with Maven:
 
    ```bash
-   # Build the entire project (includes distributions,iotdb-collector,metric-scrape,mybatis-generator)
+   # Build the entire project (includes distributions, iotdb-collector, metric-scrape, mybatis-generator, mybatis-support)
    mvn clean package -DskipTests
 
    # Or build all
@@ -107,6 +111,12 @@ IoTDB-Extras uses Maven profiles to configure different build options. You can c
   mvn clean package -Pwith-springboot -DskipTests
   ```
 
+  The default starter build uses Spring Boot 3.5.1. Validate the same starter against Spring Boot 4.1.1 with:
+
+  ```bash
+  mvn clean test -Pwith-springboot,spring-boot4 -pl iotdb-spring-boot-starter -am
+  ```
+
 - **with-examples**: Build example applications(Recommended for use in conjunction with Connector Profiles)
 
   ```bash
@@ -133,6 +143,12 @@ IoTDB-Extras uses Maven profiles to configure different build options. You can c
   mvn clean package -Pwith-flink -DskipTests
   ```
 
+- **with-mybatis**: Package the MyBatis generator plugin and runtime support as a distribution zip
+
+  ```bash
+  mvn clean package -Pwith-mybatis -DskipTests
+  ```
+
 - **with-grafana**: Build Grafana connectors and plugins
 
   ```bash
@@ -268,8 +284,9 @@ This repository includes a variety of examples demonstrating how to use IoTDB wi
 
 - **Introduction**: Shows how to use iotdb-spring-boot-starter
 - **Version**:
-  - IoTDB: 2.0.3
-  - iotdb-spring-boot-starter: 2.0.3
+  - IoTDB: 2.0.11
+  - iotdb-spring-boot-starter: 2.0.4-SNAPSHOT
+  - Starter build line: Spring Boot 3.5.1 by default; Spring Boot 4.1.1 is CI-validated
 - **Setup**:
   1. Install IoTDB
   2. Create necessary database and tables
@@ -294,8 +311,8 @@ This repository includes a variety of examples demonstrating how to use IoTDB wi
 
 - **Introduction**: Shows how to use IoTDB-Mybatis-Generator
 - **Version**:
-  - IoTDB: 2.0.2
-  - mybatis-generator-plugin: 1.3.2
+  - IoTDB: 2.0.11
+  - mybatis-generator-plugin: 2.0.4-SNAPSHOT
 - **Setup**:
   1. Install and start IoTDB
   2. Create necessary database and tables
@@ -306,6 +323,8 @@ For detailed usage instructions, please refer to the README files in the specifi
 - [Flink Examples](/examples/flink/README.md)
 - [Spark Table Examples](/examples/spark-table/README.md)
 - [MyBatis Generator Examples](/examples/mybatis-generator/README.md)
+- [MyBatis Runtime Support (query and type adapters)](/mybatis-support/README.md)
+- [MyBatis-Plus / Spring Boot 3 and 4 Example](/examples/mybatisplus-generator/README.md)
 - [IoTDB Spring Boot Starter Examples](/examples/iotdb-spring-boot-start/README.md)
 - [Kafka Examples](/examples/kafka/readme.md)
 
diff --git a/connectors/flink-iotdb-connector/pom.xml b/connectors/flink-iotdb-connector/pom.xml
index 7895abc..d332d8f 100644
--- a/connectors/flink-iotdb-connector/pom.xml
+++ b/connectors/flink-iotdb-connector/pom.xml
@@ -25,7 +25,6 @@
     <artifactId>flink-iotdb-connector</artifactId>
     <name>IoTDB: Connector: Apache Flink</name>
     <properties>
-        <compile.version>1.8</compile.version>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     </properties>
     <dependencies>
diff --git a/connectors/flink-tsfile-connector/src/main/java/org/apache/iotdb/flink/tsfile/util/TSFileConfigUtil.java b/connectors/flink-tsfile-connector/src/main/java/org/apache/iotdb/flink/tsfile/util/TSFileConfigUtil.java
index 535ff74..97591d2 100644
--- a/connectors/flink-tsfile-connector/src/main/java/org/apache/iotdb/flink/tsfile/util/TSFileConfigUtil.java
+++ b/connectors/flink-tsfile-connector/src/main/java/org/apache/iotdb/flink/tsfile/util/TSFileConfigUtil.java
@@ -40,6 +40,7 @@ public class TSFileConfigUtil {
     globalConfig.setDfsNameServices(config.getDfsNameServices());
     globalConfig.setDftSatisfyRate(config.getDftSatisfyRate());
     globalConfig.setEndian(config.getEndian());
+    globalConfig.setEncryptSalt(config.getEncryptSalt());
     globalConfig.setFloatPrecision(config.getFloatPrecision());
     globalConfig.setFreqType(config.getFreqType());
     globalConfig.setGroupSizeInByte(config.getGroupSizeInByte());
diff --git a/connectors/flink-tsfile-connector/src/test/java/org/apache/iotdb/flink/util/TSFileConfigUtilCompletenessTest.java b/connectors/flink-tsfile-connector/src/test/java/org/apache/iotdb/flink/util/TSFileConfigUtilCompletenessTest.java
index 0050e0e..2621ac3 100644
--- a/connectors/flink-tsfile-connector/src/test/java/org/apache/iotdb/flink/util/TSFileConfigUtilCompletenessTest.java
+++ b/connectors/flink-tsfile-connector/src/test/java/org/apache/iotdb/flink/util/TSFileConfigUtilCompletenessTest.java
@@ -99,7 +99,8 @@ public class TSFileConfigUtilCompletenessTest {
       "setDoubleCompression",
       "setInt32Compression",
       "setFloatCompression",
-      "setEncryptKeyFromToken"
+      "setEncryptKeyFromToken",
+      "setEncryptSalt"
     };
     Set<String> addedSetters = new HashSet<>();
     Collections.addAll(addedSetters, setters);
diff --git a/connectors/grafana-connector/pom.xml b/connectors/grafana-connector/pom.xml
index caa6e4c..57a44fc 100644
--- a/connectors/grafana-connector/pom.xml
+++ b/connectors/grafana-connector/pom.xml
@@ -37,6 +37,12 @@
         <start-class>org.apache.iotdb.web.grafana.TsfileWebDemoApplication</start-class>
     </properties>
     <dependencies>
+        <!-- JDBC is loaded by class name, so it must be packaged explicitly. -->
+        <dependency>
+            <groupId>org.apache.iotdb</groupId>
+            <artifactId>iotdb-jdbc</artifactId>
+            <scope>runtime</scope>
+        </dependency>
         <dependency>
             <groupId>org.apache.tsfile</groupId>
             <artifactId>tsfile</artifactId>
@@ -209,6 +215,16 @@
             </plugins>
         </pluginManagement>
         <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <configuration>
+                    <usedDependencies>
+                        <!-- The JDBC driver is loaded via the configured class name. -->
+                        <usedDependency>org.apache.iotdb:iotdb-jdbc</usedDependency>
+                    </usedDependencies>
+                </configuration>
+            </plugin>
             <plugin>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-maven-plugin</artifactId>
diff --git a/connectors/grafana-plugin/docker-compose.yaml b/connectors/grafana-plugin/docker-compose.yaml
index 597b4a8..3251700 100644
--- a/connectors/grafana-plugin/docker-compose.yaml
+++ b/connectors/grafana-plugin/docker-compose.yaml
@@ -29,7 +29,7 @@ services:
   # datasource in ./provisioning. The REST port serves the tree-model query
   # modes; 6667 is the native Thrift port the table-model mode uses.
   iotdb:
-    image: apache/iotdb:2.0.8-standalone
+    image: apache/iotdb:2.0.11-standalone
     container_name: iotdb
     environment:
       - dn_rpc_address=0.0.0.0
diff --git a/connectors/hive-connector/pom.xml b/connectors/hive-connector/pom.xml
index 6993547..43f8bf4 100644
--- a/connectors/hive-connector/pom.xml
+++ b/connectors/hive-connector/pom.xml
@@ -29,7 +29,6 @@
     <artifactId>hive-connector</artifactId>
     <name>IoTDB: Connector: Apache Hive</name>
     <properties>
-        <compile.version>1.8</compile.version>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     </properties>
     <dependencies>
diff --git a/connectors/hive-connector/src/main/java/org/apache/iotdb/hive/TsFileSerDe.java b/connectors/hive-connector/src/main/java/org/apache/iotdb/hive/TsFileSerDe.java
index ea08127..2d810b6 100644
--- a/connectors/hive-connector/src/main/java/org/apache/iotdb/hive/TsFileSerDe.java
+++ b/connectors/hive-connector/src/main/java/org/apache/iotdb/hive/TsFileSerDe.java
@@ -142,7 +142,7 @@ public class TsFileSerDe extends AbstractSerDe {
         PrimitiveTypeInfo pti = (PrimitiveTypeInfo) ti;
         result = PrimitiveObjectInspectorFactory.getPrimitiveJavaObjectInspector(pti);
         break;
-        // these types is not supported in TsFile
+      // these types is not supported in TsFile
       case LIST:
       case MAP:
       case STRUCT:
diff --git a/connectors/spark-iotdb-connector/pom.xml b/connectors/spark-iotdb-connector/pom.xml
index f59ee4f..4adada4 100644
--- a/connectors/spark-iotdb-connector/pom.xml
+++ b/connectors/spark-iotdb-connector/pom.xml
@@ -38,8 +38,8 @@
         <module>scala_2.12</module>
     </modules>
     <properties>
-        <maven.compiler.source>11</maven.compiler.source>
-        <maven.compiler.target>11</maven.compiler.target>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     </properties>
     <dependencies>
diff --git a/connectors/spark-iotdb-table-connector/pom.xml b/connectors/spark-iotdb-table-connector/pom.xml
index 232c9c4..d2ce80d 100644
--- a/connectors/spark-iotdb-table-connector/pom.xml
+++ b/connectors/spark-iotdb-table-connector/pom.xml
@@ -40,8 +40,8 @@
         <module>iotdb-table-connector-3.3</module>
     </modules>
     <properties>
-        <maven.compiler.source>11</maven.compiler.source>
-        <maven.compiler.target>11</maven.compiler.target>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <spark.version>3.5.0</spark.version>
     </properties>
diff --git a/distributions/pom.xml b/distributions/pom.xml
index cc002aa..ae4f4f0 100644
--- a/distributions/pom.xml
+++ b/distributions/pom.xml
@@ -119,6 +119,76 @@
                 </plugins>
             </build>
         </profile>
+        <profile>
+            <id>with-mybatis</id>
+            <dependencies>
+                <!-- Reactor ordering only: the assembly picks the jars up from the module target dirs. -->
+                <dependency>
+                    <groupId>org.apache.iotdb</groupId>
+                    <artifactId>mybatis-generator-plugin</artifactId>
+                    <version>${project.version}</version>
+                </dependency>
+                <dependency>
+                    <groupId>org.apache.iotdb</groupId>
+                    <artifactId>mybatis-support</artifactId>
+                    <version>${project.version}</version>
+                </dependency>
+            </dependencies>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-assembly-plugin</artifactId>
+                        <executions>
+                            <!-- Package binaries-->
+                            <execution>
+                                <id>all-bin</id>
+                                <goals>
+                                    <goal>single</goal>
+                                </goals>
+                                <phase>package</phase>
+                                <configuration>
+                                    <descriptors>
+                                        <descriptor>src/assembly/mybatis-generator-plugin.xml</descriptor>
+                                    </descriptors>
+                                    <finalName>apache-iotdb-${project.version}</finalName>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                    <!--
+                      Create SHA512 checksum files for the release artifacts.
+                    -->
+                    <plugin>
+                        <groupId>net.nicoulaj.maven.plugins</groupId>
+                        <artifactId>checksum-maven-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>sign-source-release</id>
+                                <goals>
+                                    <goal>files</goal>
+                                </goals>
+                                <phase>package</phase>
+                                <configuration>
+                                    <algorithms>
+                                        <algorithm>SHA-512</algorithm>
+                                    </algorithms>
+                                    <fileSets>
+                                        <!--bin-all-->
+                                        <fileSet>
+                                            <directory>${project.build.directory}</directory>
+                                            <includes>
+                                                <include>apache-iotdb-${project.version}-mybatis-generator-plugin-bin.zip</include>
+                                            </includes>
+                                        </fileSet>
+                                    </fileSets>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
         <profile>
             <id>with-flink</id>
             <build>
diff --git a/distributions/src/assembly/mybatis-generator-plugin.xml b/distributions/src/assembly/mybatis-generator-plugin.xml
index 271c084..dc00b42 100644
--- a/distributions/src/assembly/mybatis-generator-plugin.xml
+++ b/distributions/src/assembly/mybatis-generator-plugin.xml
@@ -34,6 +34,14 @@
                 <include>mybatis-generator-plugin-*.jar</include>
             </includes>
         </fileSet>
+        <!-- The generated mappers need the runtime adapters (query interceptor, DATE/BLOB handlers). -->
+        <fileSet>
+            <directory>${maven.multiModuleProjectDirectory}/mybatis-support/target/</directory>
+            <outputDirectory>${file.separator}</outputDirectory>
+            <includes>
+                <include>mybatis-support-*.jar</include>
+            </includes>
+        </fileSet>
     </fileSets>
     <componentDescriptors>
         <componentDescriptor>common-files.xml</componentDescriptor>
diff --git a/examples/iotdb-spring-boot-start/README.md b/examples/iotdb-spring-boot-start/README.md
index b9653f6..d0c5104 100644
--- a/examples/iotdb-spring-boot-start/README.md
+++ b/examples/iotdb-spring-boot-start/README.md
@@ -19,100 +19,25 @@
 
 -->
 
-# IoTDB-Spring-Boot-Starter Demo
+# Spring Boot Session example
 
-## Introduction
+This application uses Spring Boot 3.5.1, JDK 17+ and IoTDB client 2.0.11 through the locally built Extras starter (`2.0.4-SNAPSHOT`).
 
-    This demo shows how to use iotdb-spring-boot-starter
+Build from the repository root:
 
-### Version usage
+```sh
+mvn -Pwith-springboot,with-examples -pl examples/iotdb-spring-boot-start -am clean install
+```
 
-    IoTDB: 2.0.3
-    iotdb-spring-boot-starter: 2.0.3
+Start IoTDB 2.0.11. In the table dialect, create the database/table used by `IoTDBService`:
 
-### 1. Install IoTDB
+```sql
+CREATE DATABASE IF NOT EXISTS wind;
+USE wind;
+CREATE TABLE IF NOT EXISTS power_data_set (device STRING TAG, value DOUBLE FIELD);
+INSERT INTO power_data_set(time, device, value) VALUES (1, 'demo', 42.0);
+```
 
-    please refer to [https://iotdb.apache.org/#/Download](https://iotdb.apache.org/#/Download)
+Edit `src/main/resources/application.properties` for your endpoints and credentials, then run `IoTDBSpringBootStartApplication` from the IDE or `mvn spring-boot:run` in this directory. The service exposes `queryTableSessionPool()` and `querySessionPool()` for invocation from application code; startup itself does not issue those queries.
 
-### 2. Startup IoTDB
-
-    please refer to [Quick Start](http://iotdb.apache.org/UserGuide/Master/Get%20Started/QuickStart.html)
-
-    Then we need to create a database 'wind' by cli in table model
-    ```
-    create database wind;
-    use wind;
-    ```
-    Then we need to create a database 'table'
-    ```
-    CREATE TABLE table1 (
-        time TIMESTAMP TIME,
-        region STRING TAG,
-        plant_id STRING TAG,
-        device_id STRING TAG,
-        model_id STRING ATTRIBUTE,
-        maintenance STRING ATTRIBUTE,
-        temperature FLOAT FIELD,
-        humidity FLOAT FIELD,
-        status Boolean FIELD,
-        arrival_time TIMESTAMP FIELD
-    ) WITH (TTL=31536000000);
-    ```
-
-### 3. Build Dependencies with Maven in your Project
-
-    ```
-        <dependencies>
-            <dependency>
-                <groupId>org.springframework.boot</groupId>
-                <artifactId>spring-boot-starter</artifactId>
-            </dependency>
-
-            <dependency>
-                <groupId>org.springframework.boot</groupId>
-                <artifactId>spring-boot-starter-test</artifactId>
-                <scope>test</scope>
-            </dependency>
-            <dependency>
-                <groupId>org.apache.iotdb</groupId>
-                <artifactId>iotdb-spring-boot-starter</artifactId>
-                <version>2.0.3</version>
-            </dependency>
-        </dependencies>
-    ```
-
-### 4、Use The target Bean with @Autowired
-
-    You can use the target Bean in your Project,like:
-    ```
-        @Autowired
-        private ITableSessionPool ioTDBSessionPool;
-        @Autowired
-        private SessionPool sessionPool;
-
-        public void queryTableSessionPool() throws IoTDBConnectionException, StatementExecutionException {
-            ITableSession tableSession = ioTDBSessionPool.getSession();
-            final SessionDataSet sessionDataSet = tableSession.executeQueryStatement("select * from power_data_set limit 10");
-            while (sessionDataSet.hasNext()) {
-                final RowRecord rowRecord = sessionDataSet.next();
-                final List<Field> fields = rowRecord.getFields();
-                for (Field field : fields) {
-                    System.out.print(field.getStringValue());
-                }
-                System.out.println();
-            }
-        }
-
-        public void querySessionPool() throws IoTDBConnectionException, StatementExecutionException {
-            final SessionDataSetWrapper sessionDataSetWrapper = sessionPool.executeQueryStatement("show databases");
-            while (sessionDataSetWrapper.hasNext()) {
-                final RowRecord rowRecord = sessionDataSetWrapper.next();
-                final List<Field> fields = rowRecord.getFields();
-                for (Field field : fields) {
-                    System.out.print(field.getStringValue());
-                }
-                System.out.println();
-            }
-        }
-
-    ```
+Both methods close results and return borrowed sessions even on exceptions. The commented live-query test requires a server and is not part of the offline unit-test suite. See the [starter reference](../../iotdb-spring-boot-starter/README.md) for configuration defaults, custom pools and transaction limitations.
diff --git a/examples/iotdb-spring-boot-start/pom.xml b/examples/iotdb-spring-boot-start/pom.xml
index 4fe181c..8de45bf 100644
--- a/examples/iotdb-spring-boot-start/pom.xml
+++ b/examples/iotdb-spring-boot-start/pom.xml
@@ -30,15 +30,15 @@
     </parent>
     <groupId>org.apache.iotdb</groupId>
     <artifactId>iotdb-spring-boot-start-example</artifactId>
-    <version>2.0.4-SNAPHOT</version>
+    <version>2.0.4-SNAPSHOT</version>
     <name>IoTDB: Example: SpringBoot Starter</name>
     <description>iotdb-spring-boot-start</description>
     <properties>
         <java.version>17</java.version>
         <version>3.5.1</version>
-        <google.java.format.version>1.22.0</google.java.format.version>
-        <spotless.version>2.43.0</spotless.version>
-        <iotdb.version>2.0.5</iotdb.version>
+        <google.java.format.version>1.28.0</google.java.format.version>
+        <spotless.version>2.44.5</spotless.version>
+        <iotdb.version>2.0.11</iotdb.version>
     </properties>
     <dependencies>
         <dependency>
diff --git a/examples/iotdb-spring-boot-start/src/main/java/org/apache/iotdb/iotdbspringbootstartexample/service/IoTDBService.java b/examples/iotdb-spring-boot-start/src/main/java/org/apache/iotdb/iotdbspringbootstartexample/service/IoTDBService.java
index f749e88..3c2247a 100644
--- a/examples/iotdb-spring-boot-start/src/main/java/org/apache/iotdb/iotdbspringbootstartexample/service/IoTDBService.java
+++ b/examples/iotdb-spring-boot-start/src/main/java/org/apache/iotdb/iotdbspringbootstartexample/service/IoTDBService.java
@@ -40,32 +40,31 @@ public class IoTDBService {
   @Autowired private ISessionPool sessionPool;
 
   public void queryTableSessionPool() throws IoTDBConnectionException, StatementExecutionException {
-    ITableSession tableSession = ioTDBSessionPool.getSession();
-    final SessionDataSet sessionDataSet =
-        tableSession.executeQueryStatement("select * from power_data_set limit 10");
-    while (sessionDataSet.hasNext()) {
-      final RowRecord rowRecord = sessionDataSet.next();
-      final List<Field> fields = rowRecord.getFields();
-      for (Field field : fields) {
-        System.out.print(field.getStringValue());
+    try (ITableSession tableSession = ioTDBSessionPool.getSession();
+        SessionDataSet sessionDataSet =
+            tableSession.executeQueryStatement("select * from power_data_set limit 10")) {
+      while (sessionDataSet.hasNext()) {
+        final RowRecord rowRecord = sessionDataSet.next();
+        final List<Field> fields = rowRecord.getFields();
+        for (Field field : fields) {
+          System.out.print(field.getStringValue());
+        }
+        System.out.println();
       }
-      System.out.println();
     }
-    sessionDataSet.close();
-    tableSession.close();
   }
 
   public void querySessionPool() throws IoTDBConnectionException, StatementExecutionException {
-    final SessionDataSetWrapper sessionDataSetWrapper =
-        sessionPool.executeQueryStatement("show databases");
-    while (sessionDataSetWrapper.hasNext()) {
-      final RowRecord rowRecord = sessionDataSetWrapper.next();
-      final List<Field> fields = rowRecord.getFields();
-      for (Field field : fields) {
-        System.out.print(field.getStringValue());
+    try (SessionDataSetWrapper sessionDataSetWrapper =
+        sessionPool.executeQueryStatement("show databases")) {
+      while (sessionDataSetWrapper.hasNext()) {
+        final RowRecord rowRecord = sessionDataSetWrapper.next();
+        final List<Field> fields = rowRecord.getFields();
+        for (Field field : fields) {
+          System.out.print(field.getStringValue());
+        }
+        System.out.println();
       }
-      System.out.println();
     }
-    sessionDataSetWrapper.close();
   }
 }
diff --git a/examples/iotdb-spring-boot-start/src/main/resources/application.properties b/examples/iotdb-spring-boot-start/src/main/resources/application.properties
index 0680ea7..5185184 100644
--- a/examples/iotdb-spring-boot-start/src/main/resources/application.properties
+++ b/examples/iotdb-spring-boot-start/src/main/resources/application.properties
@@ -18,9 +18,8 @@
 
 spring.application.name=iotdb-spring-boot-start
 
-iotdb.session.node_urls=127.0.0.1:6667
+iotdb.session.node-urls=127.0.0.1:6667
 iotdb.session.password=root
 iotdb.session.username=root
 iotdb.session.database=wind
-iotdb.session.sql_dialect=table
-iotdb.session.max_size=10
\ No newline at end of file
+iotdb.session.max-size=10
diff --git a/examples/kafka/readme.md b/examples/kafka/readme.md
index 765a0cb..7269d24 100644
--- a/examples/kafka/readme.md
+++ b/examples/kafka/readme.md
@@ -28,7 +28,7 @@ The example is to show how to send data from localhost to IoTDB through Kafka.
 
 |       | Version |
 |-------|---------|
-| IoTDB | 2.0.5   |  
+| IoTDB | 2.0.11   |
 | Kafka | 2.8.2   |
 
 ### Dependencies with Maven
@@ -43,7 +43,7 @@ The example is to show how to send data from localhost to IoTDB through Kafka.
     <dependency>
         <groupId>org.apache.iotdb</groupId>
         <artifactId>iotdb-session</artifactId>
-        <version>2.0.5</version>
+        <version>2.0.11</version>
     </dependency>
 </dependencies>
 ```
@@ -132,4 +132,4 @@ Step 2: Run `RelationalConsumer.java`
 
 
 ### Notice 
-If you want to use multiple consumers, please make sure that the number of topic's partition you create is more than 1.
\ No newline at end of file
+If you want to use multiple consumers, please make sure that the number of topic's partition you create is more than 1.
diff --git a/examples/mybatis-generator/README.md b/examples/mybatis-generator/README.md
index 234f0a6..3b1a6ad 100644
--- a/examples/mybatis-generator/README.md
+++ b/examples/mybatis-generator/README.md
@@ -19,102 +19,78 @@
 
 -->
 
-# Mybatis-Generator Demo
+# MyBatis Generator example
 
-## Introduction
+Requires **JDK 17+, IoTDB/JDBC 2.0.11 and MyBatis 3.5.19**. The build-time generator and runtime support artifacts use Extras version `2.0.4-SNAPSHOT`.
 
-This demo shows how to use IoTDB-Mybatis-Generator
+## Prepare the schema
 
-### Version usage
+Start IoTDB with the table dialect and create:
 
-IoTDB: 2.0.2
-mybatis-generator-plugin: 1.3.2
+```sql
+CREATE DATABASE IF NOT EXISTS test;
+USE test;
+CREATE TABLE IF NOT EXISTS mix (
+    device_id STRING TAG,
+    region STRING ATTRIBUTE, plant_id STRING ATTRIBUTE,
+    model_id STRING ATTRIBUTE, maintenance STRING ATTRIBUTE,
+    temperature FLOAT FIELD, humidity FLOAT FIELD, status BOOLEAN FIELD,
+    arrival_time TIMESTAMP FIELD, reading_date DATE FIELD, payload BLOB FIELD
+);
+```
 
-### 1. Install IoTDB
+If `mix` already exists from an older example, add the missing `reading_date DATE FIELD` and `payload BLOB FIELD` columns with ALTER TABLE; CREATE IF NOT EXISTS does not update an existing schema.
 
-please refer to [https://iotdb.apache.org/#/Download](https://iotdb.apache.org/#/Download)
+From the repository root:
 
-### 2. Startup IoTDB
+```sh
+mvn -Pwith-examples -pl examples/mybatis-generator -am clean install
+```
 
-please refer to [Quick Start](http://iotdb.apache.org/UserGuide/Master/Get%20Started/QuickStart.html)
+Set the endpoint, database and credentials in `src/main/resources/generatorConfig.xml` and `mybatis-config.xml`. The latter registers `IoTDBQueryInterceptor` for the 2.0.11 prepared-query behavior. XML is included in the packaged JAR by the example's POM.
 
-Then we need to create a database 'test' by cli in table model
-```
-create database test;
-use test;
-```
-Then we need to create a database 'table'
-```
-CREATE TABLE mix (
-    time TIMESTAMP TIME,
-    region STRING TAG,
-    plant_id STRING TAG,
-    device_id STRING TAG,
-    model_id STRING ATTRIBUTE,
-    maintenance STRING ATTRIBUTE,
-    temperature FLOAT FIELD,
-    humidity FLOAT FIELD,
-    status Boolean FIELD,
-    arrival_time TIMESTAMP FIELD
-);
-```
+Run `org.apache.iotdb.mybatis.Main` from the IDE. Its timestamp literals assume the default **ms** server precision; use matching raw Long values on us/ns servers.
+
+## Generate and migrate
+
+From this example directory:
 
-### 3. Build Dependencies with Maven in your Project
-
-```xml
-<build>
-    <plugins>
-        <plugin>
-            <groupId>org.mybatis.generator</groupId>
-            <artifactId>mybatis-generator-maven-plugin</artifactId>
-            <version>1.4.2</version>
-            <dependencies>
-                <dependency>
-                    <groupId>org.apache.iotdb</groupId>
-                    <artifactId>mybatis-generator-plugin</artifactId>
-                    <version>1.3.2</version>
-                </dependency>
-            </dependencies>
-            <configuration>
-                <verbose>true</verbose>
-                <overwrite>true</overwrite>
-                <configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
-            </configuration>
-        </plugin>
-    </plugins>
-</build>
+```sh
+mvn mybatis-generator:generate
+mvn test
 ```
 
-### 5. put The generatorConfig.xml in your project
+Generation may overwrite the model, interface and XML; review all three together. `UnmergeableXmlMappersPlugin` replaces generated XML on reruns to prevent duplicate statement IDs. The default runtime is `MyBatis3Simple`. For Example/criteria generation:
 
-- `src/main/resources/generatorConfig.xml`
+```sh
+mvn mybatis-generator:generate -Dmybatis.generator.configurationFile=src/main/resources/generatorConfigByExample.xml
+```
 
-each table generates an entity object
+Both configurations specify TIME + `device_id` as the key, use `IoTDBKeyPlugin` for nullable TAG predicates, and disable generic UPDATE. `ignoreQualifiersAtRuntime=true` selects the runtime database from the JDBC URL.
 
-- `src/main/resources/generatorConfigByExample.xml`
+The Java API now uses **Long** for TIME/TIMESTAMP, **Float** for FLOAT, **LocalDate** for DATE and **byte[]** for BLOB. Update callers that previously supplied Date/Double. DATE/BLOB handlers are emitted in both parameter and result mappings; the runtime `mybatis-support` dependency is required after generation.
 
-The generated object will contain many "by Example" methods. If you do not want to generate these, you can configure to cancel them in the subsequent table elements
+`batchInsert(records)` validates before writing and splits at 500 rows. Empty input returns 0; null input/elements throw before SQL. `batchInsertRows` is an internal statement: regenerate the interface and XML together when upgrading. JDBC reports unknown affected-row counts as -1; a negative result alone is not a SQL failure.
 
-### 6. generate generates corresponding Java classes and mapper files
+FIELD changes use INSERT on the same key. Generic UPDATE is disabled because IoTDB UPDATE supports ATTRIBUTE only. ATTRIBUTE values belong to the device across timestamps. Inserts, chunks and transaction annotations provide no rollback guarantee.
 
-exec `mvn mybatis-generator:generate`
+The checked-in `selectAll` has LIMIT 1000. MBG's standard regenerated `selectAll`/Example queries do not automatically retain this cap; add time/device predicates and limits appropriate to your workload.
 
-Execute the command at the location of the 'pom' in the project: Mvn mybatis generator: generate generates corresponding Java classes and mapper files
+## Tests
 
-### 7、the target file location
+Default tests are offline and validate the packaged XML, interceptor registration, key predicates and handlers. Plugin/runtime tests also compile generated batch methods and cover empty/null inputs, chunking, quoting and type handling.
 
-You can see the target file in your Project  
+Real-server tests are explicit and fail if the server is unavailable:
 
-```
-org/apache/iotdb/mybatis/plugin/model/Mix.java
-org/apache/iotdb/mybatis/plugin/mapper/MixMapper.java
-org/apache/iotdb/mybatis/plugin/xml/MixMapper.xml
+```sh
+# From the repository root; the account must be able to create/drop a test database.
+mvn -Pwith-examples,iotdb-mybatis-it -pl examples/mybatis-generator -am verify \
+  '-Diotdb.it.url=jdbc:iotdb://127.0.0.1:6667/?sql_dialect=table' \
+  -Diotdb.it.precision=ms
 ```
 
-if you are using the 'src/main/resources/generatorConfiguraByExample. xml' file`, You can see the target file in your Project  
-```
-org/apache/iotdb/mybatis/plugin/model/Mix.java
-org/apache/iotdb/mybatis/plugin/model/MixExample.java
-org/apache/iotdb/mybatis/plugin/mapper/MixMapper.java
-org/apache/iotdb/mybatis/plugin/xml/MixMapper.xml
-```
+Optional properties: `iotdb.it.username` / `iotdb.it.password` (default root/root), `iotdb.it.precision` (ms/us/ns; must match the server). Tests create a unique database and drop only that database, generate from live metadata into a temporary directory, compile the result, and exercise multi-chunk inserts, composite keys, reserved names, DATE/BLOB/NULL and raw timestamps.
+
+On official IoTDB 2.0.11 servers configured with **us/ns precision**, a DELETE with a time predicate can report success without deleting the matching row. This server routing issue also reproduces with raw JDBC. The strict deletion assertions expose it; the **ms** suite passes. Reads and writes preserve raw Long timestamp precision, but key deletion on us/ns servers requires a server-side fix. Do not remove the time predicate as a workaround: doing so can delete other rows for the same device.
+
+See the [plugin reference](../../mybatis-generator/README.md) and [runtime adapters](../../mybatis-support/README.md).
diff --git a/examples/mybatis-generator/pom.xml b/examples/mybatis-generator/pom.xml
index 0a69db5..0828c79 100644
--- a/examples/mybatis-generator/pom.xml
+++ b/examples/mybatis-generator/pom.xml
@@ -29,8 +29,33 @@
     </parent>
     <artifactId>mybatis-generator-example</artifactId>
     <name>IoTDB: Example: Mybatis Generator</name>
-    <version>2.0.4-SNAPHOT</version>
+    <version>2.0.4-SNAPSHOT</version>
+    <properties>
+        <mybatis.generator.configurationFile>src/main/resources/generatorConfig.xml</mybatis.generator.configurationFile>
+    </properties>
     <dependencies>
+        <dependency>
+            <groupId>org.apache.iotdb</groupId>
+            <artifactId>mybatis-generator-plugin</artifactId>
+            <version>2.0.4-SNAPSHOT</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mybatis.generator</groupId>
+            <artifactId>mybatis-generator-core</artifactId>
+            <version>1.4.2</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.iotdb</groupId>
+            <artifactId>mybatis-support</artifactId>
+            <version>2.0.4-SNAPSHOT</version>
+        </dependency>
         <dependency>
             <groupId>org.mybatis</groupId>
             <artifactId>mybatis</artifactId>
@@ -69,11 +94,26 @@
             </resource>
         </resources>
         <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>integration-tests</id>
+                        <phase>none</phase>
+                    </execution>
+                </executions>
+            </plugin>
             <plugin>
                 <groupId>org.mybatis.generator</groupId>
                 <artifactId>mybatis-generator-maven-plugin</artifactId>
                 <version>1.4.2</version>
                 <dependencies>
+                    <dependency>
+                        <groupId>org.apache.iotdb</groupId>
+                        <artifactId>iotdb-jdbc</artifactId>
+                        <version>${iotdb.version}</version>
+                    </dependency>
                     <dependency>
                         <groupId>org.apache.iotdb</groupId>
                         <artifactId>mybatis-generator-plugin</artifactId>
@@ -83,7 +123,7 @@
                 <configuration>
                     <verbose>true</verbose>
                     <overwrite>true</overwrite>
-                    <configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
+                    <configurationFile>${mybatis.generator.configurationFile}</configurationFile>
                 </configuration>
             </plugin>
             <plugin>
@@ -94,6 +134,10 @@
                         <!-- These are used at runtime in tests -->
                         <usedDependency>org.apache.iotdb:iotdb-jdbc</usedDependency>
                     </usedDependencies>
+                    <ignoredNonTestScopedDependencies>
+                        <!-- The production mapper XML also uses these handlers. -->
+                        <ignoredNonTestScopedDependency>org.apache.iotdb:mybatis-support</ignoredNonTestScopedDependency>
+                    </ignoredNonTestScopedDependencies>
                 </configuration>
             </plugin>
             <plugin>
@@ -105,4 +149,26 @@
             </plugin>
         </plugins>
     </build>
+    <profiles>
+        <profile>
+            <id>iotdb-mybatis-it</id>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-failsafe-plugin</artifactId>
+                        <version>3.5.3</version>
+                        <executions>
+                            <execution>
+                                <goals>
+                                    <goal>integration-test</goal>
+                                    <goal>verify</goal>
+                                </goals>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
 </project>
diff --git a/examples/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/Main.java b/examples/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/Main.java
index 5fb32d6..e08c7db 100644
--- a/examples/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/Main.java
+++ b/examples/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/Main.java
@@ -30,25 +30,24 @@ import org.apache.ibatis.session.SqlSessionFactoryBuilder;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
-import java.util.Date;
 import java.util.List;
 
 public class Main {
   public static void main(String[] args) throws IOException {
     String resource = "mybatis-config.xml";
-    InputStream inputStream = Resources.getResourceAsStream(resource);
-    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
+    SqlSessionFactory sqlSessionFactory;
+    try (InputStream inputStream = Resources.getResourceAsStream(resource)) {
+      sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
+    }
 
     try (SqlSession session = sqlSessionFactory.openSession(true)) {
       MixMapper mapper = session.getMapper(MixMapper.class);
 
-      Date now = new Date();
-
       // 1. insertOne
       System.out.println("1. insertOne...");
-      insertOne(mapper, new Date(1));
-      insertOne(mapper, new Date(2));
-      insertOne(mapper, new Date(3));
+      insertOne(mapper, 1L);
+      insertOne(mapper, 2L);
+      insertOne(mapper, 3L);
       System.out.println("------------------\n");
 
       // 2. selectAll
@@ -58,27 +57,27 @@ public class Main {
 
       // 3. selectByPrimaryKey
       System.out.println("3. selectByPrimaryKey...");
-      selectByPrimaryKey(mapper, new Date(1), "dev001");
+      selectByPrimaryKey(mapper, 1L, "dev001");
       System.out.println("------------------\n");
 
       // 4. deleteByPrimaryKey
       System.out.println("4. deleteByPrimaryKey...");
-      deleteByPrimaryKey(mapper, new Date(1), "dev001");
-      deleteByPrimaryKey(mapper, new Date(2), "dev001");
+      deleteByPrimaryKey(mapper, 1L, "dev001");
+      deleteByPrimaryKey(mapper, 2L, "dev001");
       System.out.println("selectAll...");
       selectAll(mapper);
       System.out.println("------------------\n");
 
       // 5. batchInsert
       System.out.println("5. batchInsert...");
-      batchInsert(mapper, new Date(6));
+      batchInsert(mapper, 6L);
       System.out.println("selectAll...");
       selectAll(mapper);
       System.out.println("------------------\n");
     }
   }
 
-  private static void insertOne(MixMapper mapper, Date now) {
+  private static void insertOne(MixMapper mapper, Long now) {
     Mix mix = new Mix();
     mix.setTime(now);
     mix.setDeviceId("dev001");
@@ -86,8 +85,8 @@ public class Main {
     mix.setPlantId("plantA");
     mix.setModelId("modelX");
     mix.setMaintenance("正常");
-    mix.setTemperature(25.5);
-    mix.setHumidity(60.0);
+    mix.setTemperature(25.5f);
+    mix.setHumidity(60.0f);
     mix.setStatus(true);
     mix.setArrivalTime(now);
     mapper.insert(mix);
@@ -98,29 +97,29 @@ public class Main {
     all.forEach(System.out::println);
   }
 
-  private static void selectByPrimaryKey(MixMapper mapper, Date time, String deviceId) {
+  private static void selectByPrimaryKey(MixMapper mapper, Long time, String deviceId) {
     Mix one = mapper.selectByPrimaryKey(time, deviceId);
     System.out.println("results: " + one);
   }
 
-  private static void deleteByPrimaryKey(MixMapper mapper, Date time, String deviceId) {
+  private static void deleteByPrimaryKey(MixMapper mapper, Long time, String deviceId) {
     mapper.deleteByPrimaryKey(time, deviceId);
   }
 
-  private static void batchInsert(MixMapper mapper, Date now) {
+  private static void batchInsert(MixMapper mapper, Long now) {
     List<Mix> batchList = new ArrayList<>();
     for (int i = 0; i < 3; i++) {
       Mix m = new Mix();
-      m.setTime(new Date(now.getTime() + i * 1000));
+      m.setTime(now + i * 1000);
       m.setDeviceId("dev00" + (i + 2));
       m.setRegion("华东");
       m.setPlantId("plantA");
       m.setModelId("modelX");
       m.setMaintenance("正常");
-      m.setTemperature(20.0 + i);
-      m.setHumidity(50.0 + i);
+      m.setTemperature(20.0f + i);
+      m.setHumidity(50.0f + i);
       m.setStatus(i % 2 == 0);
-      m.setArrivalTime(new Date(now.getTime() + i * 1000));
+      m.setArrivalTime(now + i * 1000);
       batchList.add(m);
     }
     mapper.batchInsert(batchList);
diff --git a/examples/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/plugin/mapper/MixMapper.java b/examples/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/plugin/mapper/MixMapper.java
index fb54c92..7537b83 100644
--- a/examples/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/plugin/mapper/MixMapper.java
+++ b/examples/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/plugin/mapper/MixMapper.java
@@ -23,17 +23,30 @@ import org.apache.iotdb.mybatis.plugin.model.Mix;
 
 import org.apache.ibatis.annotations.Param;
 
-import java.util.Date;
 import java.util.List;
 
 public interface MixMapper {
-  int deleteByPrimaryKey(@Param("time") Date time, @Param("deviceId") String deviceId);
+  int deleteByPrimaryKey(@Param("time") Long time, @Param("deviceId") String deviceId);
 
   int insert(Mix row);
 
-  Mix selectByPrimaryKey(@Param("time") Date time, @Param("deviceId") String deviceId);
+  Mix selectByPrimaryKey(@Param("time") Long time, @Param("deviceId") String deviceId);
 
   List<Mix> selectAll();
 
-  int batchInsert(@Param("records") List<Mix> records);
+  default int batchInsert(List<Mix> records) {
+    if (records == null || records.stream().anyMatch(java.util.Objects::isNull)) {
+      throw new IllegalArgumentException("records and its elements must not be null");
+    }
+    int result = 0;
+    for (int start = 0; start < records.size(); ) {
+      int end = start + Math.min(500, records.size() - start);
+      int count = batchInsertRows(records.subList(start, end));
+      result = count < 0 || result < 0 ? -1 : result + count;
+      start = end;
+    }
+    return result;
+  }
+
+  int batchInsertRows(@Param("records") List<Mix> records);
 }
diff --git a/examples/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/plugin/model/Mix.java b/examples/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/plugin/model/Mix.java
index d32e3e4..b6e83c6 100644
--- a/examples/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/plugin/model/Mix.java
+++ b/examples/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/plugin/model/Mix.java
@@ -23,7 +23,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
 import java.io.Serializable;
-import java.util.Date;
+import java.time.LocalDate;
 
 /**
  * table: test.mix of model class
@@ -37,7 +37,7 @@ public class Mix implements Serializable {
   /** class serial version id */
   private static final long serialVersionUID = 1L;
 
-  private Date time;
+  private Long time;
 
   private String deviceId;
 
@@ -49,11 +49,15 @@ public class Mix implements Serializable {
 
   private String maintenance;
 
-  private Double temperature;
+  private Float temperature;
 
-  private Double humidity;
+  private Float humidity;
 
   private Boolean status;
 
-  private Date arrivalTime;
+  private Long arrivalTime;
+
+  private LocalDate readingDate;
+
+  private byte[] payload;
 }
diff --git a/examples/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/plugin/xml/MixMapper.xml b/examples/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/plugin/xml/MixMapper.xml
index ea23c97..a245954 100644
--- a/examples/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/plugin/xml/MixMapper.xml
+++ b/examples/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/plugin/xml/MixMapper.xml
@@ -1,40 +1,51 @@
-<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
 
 <!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
 
-       Licensed to the Apache Software Foundation (ASF) under one or more
-
-       contributor license agreements.  See the NOTICE file distributed with
-       this work for additional information regarding copyright ownership.
-       The ASF licenses this file to You under the Apache License, Version 2.0
-
-       (the "License"); you may not use this file except in compliance with
-
-       the License.  You may obtain a copy of the License at
-            http://www.apache.org/licenses/LICENSE-2.0
-       Unless required by applicable law or agreed to in writing, software
-       distributed under the License is distributed on an "AS IS" BASIS,
-       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     http://www.apache.org/licenses/LICENSE-2.0
 
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
 -->
 <mapper namespace="org.apache.iotdb.mybatis.plugin.mapper.MixMapper">
     <resultMap id="BaseResultMap" type="org.apache.iotdb.mybatis.plugin.model.Mix">
-        <id column="time" jdbcType="TIMESTAMP" property="time"/>
-        <id column="device_id" jdbcType="VARCHAR" property="deviceId"/>
-        <result column="region" jdbcType="VARCHAR" property="region"/>
-        <result column="plant_id" jdbcType="VARCHAR" property="plantId"/>
-        <result column="model_id" jdbcType="VARCHAR" property="modelId"/>
-        <result column="maintenance" jdbcType="VARCHAR" property="maintenance"/>
-        <result column="temperature" jdbcType="FLOAT" property="temperature"/>
-        <result column="humidity" jdbcType="FLOAT" property="humidity"/>
-        <result column="status" jdbcType="BOOLEAN" property="status"/>
-        <result column="arrival_time" jdbcType="TIMESTAMP" property="arrivalTime"/>
+        <id column="time" property="time" jdbcType="TIMESTAMP"/>
+        <id column="device_id" property="deviceId" jdbcType="VARCHAR"/>
+        <result column="region" property="region" jdbcType="VARCHAR"/>
+        <result column="plant_id" property="plantId" jdbcType="VARCHAR"/>
+        <result column="model_id" property="modelId" jdbcType="VARCHAR"/>
+        <result column="maintenance" property="maintenance" jdbcType="VARCHAR"/>
+        <result column="temperature" property="temperature" jdbcType="FLOAT"/>
+        <result column="humidity" property="humidity" jdbcType="FLOAT"/>
+        <result column="status" property="status" jdbcType="BOOLEAN"/>
+        <result column="arrival_time" property="arrivalTime" jdbcType="TIMESTAMP"/>
+        <result column="reading_date" property="readingDate" jdbcType="DATE" typeHandler="org.apache.iotdb.mybatis.type.IoTDBLocalDateTypeHandler"/>
+        <result column="payload" property="payload" jdbcType="BLOB" typeHandler="org.apache.iotdb.mybatis.type.IoTDBBlobTypeHandler"/>
     </resultMap>
-    <delete id="deleteByPrimaryKey" parameterType="map">delete from test.mix where time = #{time,jdbcType=TIMESTAMP} and device_id = #{deviceId,jdbcType=VARCHAR}</delete>
-    <insert id="insert" parameterType="org.apache.iotdb.mybatis.plugin.model.Mix">insert into test.mix (time, device_id, region, plant_id, model_id, maintenance, temperature, humidity, status, arrival_time) values (#{time,jdbcType=TIMESTAMP}, #{deviceId,jdbcType=VARCHAR}, #{region,jdbcType=VARCHAR}, #{plantId,jdbcType=VARCHAR}, #{modelId,jdbcType=VARCHAR}, #{maintenance,jdbcType=VARCHAR}, #{temperature,jdbcType=FLOAT}, #{humidity,jdbcType=FLOAT}, #{status,jdbcType=BOOLEAN}, #{arrivalTime [...]
-    <select id="selectByPrimaryKey" parameterType="map" resultMap="BaseResultMap">select time, device_id, region, plant_id, model_id, maintenance, temperature, humidity, status, arrival_time from test.mix where time = #{time,jdbcType=TIMESTAMP} and device_id = #{deviceId,jdbcType=VARCHAR}</select>
-    <select id="selectAll" resultMap="BaseResultMap">select time, device_id, region, plant_id, model_id, maintenance, temperature, humidity, status, arrival_time from test.mix</select>
-    <insert id="batchInsert" parameterType="java.util.List">insert into test.mix ( time, device_id, region, plant_id, model_id, maintenance, temperature, humidity, status, arrival_time ) values
-        <foreach collection="records" index="index" item="item" separator=",">( #{item.time,jdbcType=TIMESTAMP}, #{item.deviceId,jdbcType=VARCHAR}, #{item.region,jdbcType=VARCHAR}, #{item.plantId,jdbcType=VARCHAR}, #{item.modelId,jdbcType=VARCHAR}, #{item.maintenance,jdbcType=VARCHAR}, #{item.temperature,jdbcType=FLOAT}, #{item.humidity,jdbcType=FLOAT}, #{item.status,jdbcType=BOOLEAN}, #{item.arrivalTime,jdbcType=TIMESTAMP} )</foreach>
+    <sql id="Key">"time" = #{time,jdbcType=TIMESTAMP}
+        <choose>
+            <when test="deviceId != null">AND "device_id" = #{deviceId,jdbcType=VARCHAR}</when>
+            <otherwise>AND "device_id" IS NULL</otherwise>
+        </choose>
+    </sql>
+    <delete id="deleteByPrimaryKey">DELETE FROM "mix" WHERE
+        <include refid="Key"/>
+    </delete>
+    <insert id="insert">INSERT INTO "mix" ("time", "region", "plant_id", "device_id", "model_id", "maintenance", "temperature", "humidity", "status", "arrival_time", "reading_date", "payload") VALUES (#{time,jdbcType=TIMESTAMP}, #{region,jdbcType=VARCHAR}, #{plantId,jdbcType=VARCHAR}, #{deviceId,jdbcType=VARCHAR}, #{modelId,jdbcType=VARCHAR}, #{maintenance,jdbcType=VARCHAR}, #{temperature,jdbcType=FLOAT}, #{humidity,jdbcType=FLOAT}, #{status,jdbcType=BOOLEAN}, #{arrivalTime,jdbcType=TIME [...]
+    <select id="selectByPrimaryKey" resultMap="BaseResultMap">SELECT * FROM "mix" WHERE
+        <include refid="Key"/>
+    </select>
+    <select id="selectAll" resultMap="BaseResultMap">SELECT * FROM "mix" ORDER BY time, device_id LIMIT 1000</select>
+    <insert id="batchInsertRows">INSERT INTO "mix" ("time", "region", "plant_id", "device_id", "model_id", "maintenance", "temperature", "humidity", "status", "arrival_time", "reading_date", "payload") VALUES
+        <foreach collection="records" item="item" separator=",">(#{item.time,jdbcType=TIMESTAMP}, #{item.region,jdbcType=VARCHAR}, #{item.plantId,jdbcType=VARCHAR}, #{item.deviceId,jdbcType=VARCHAR}, #{item.modelId,jdbcType=VARCHAR}, #{item.maintenance,jdbcType=VARCHAR}, #{item.temperature,jdbcType=FLOAT}, #{item.humidity,jdbcType=FLOAT}, #{item.status,jdbcType=BOOLEAN}, #{item.arrivalTime,jdbcType=TIMESTAMP}, #{item.readingDate,jdbcType=DATE,typeHandler=org.apache.iotdb.mybatis.type.IoT [...]
     </insert>
 </mapper>
diff --git a/examples/mybatis-generator/src/main/resources/generatorConfig.xml b/examples/mybatis-generator/src/main/resources/generatorConfig.xml
index 6ad1c10..6fe8953 100644
--- a/examples/mybatis-generator/src/main/resources/generatorConfig.xml
+++ b/examples/mybatis-generator/src/main/resources/generatorConfig.xml
@@ -18,7 +18,7 @@
 
 -->
 <generatorConfiguration>
-    <classPathEntry location="/path/to/iotdb-jdbc.jar"/>
+    <!-- The Maven generator plugin supplies iotdb-jdbc on its plugin classpath. -->
     <!-- mvn mybatis-generator:generate hierarchical/flat-->
     <context id="myBatis3Simple" targetRuntime="MyBatis3Simple" defaultModelType="flat">
         <!--        <property name="suppressAllComments" value="true"/>-->
@@ -27,6 +27,8 @@
         <!--        <property name="xmlFormatter" value="org.mybatis.generator.api.dom.DefaultXmlFormatter"/>-->
         <plugin type="org.apache.iotdb.mybatis.plugin.LombokPlugin"/>
         <plugin type="org.apache.iotdb.mybatis.plugin.BatchInsertPlugin"/>
+        <plugin type="org.apache.iotdb.mybatis.plugin.IoTDBKeyPlugin"/>
+        <plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin"/>
         <!--        <plugin type="org.apache.iotdb.mybatis.plugin.SerializablePlugin"/>-->
         <plugin type="org.mybatis.generator.plugins.VirtualPrimaryKeyPlugin"/>
         <!--        <commentGenerator type="generator.org.apache.iotdb.mybatis.plugin.CNCommentGenerator">-->
@@ -40,6 +42,8 @@
         <jdbcConnection driverClass="org.apache.iotdb.jdbc.IoTDBDriver" connectionURL="jdbc:iotdb://127.0.0.1:6667/test?sql_dialect=table" userId="root" password="root"/>
         <javaTypeResolver type="org.apache.iotdb.mybatis.plugin.generator.resolver.IoTDBJavaTypeResolver">
             <property name="forceBigDecimals" value="false"/>
+            <property name="jdbcType.TIMESTAMP" value="java.lang.Long"/>
+            <property name="jdbcType.FLOAT" value="java.lang.Float"/>
         </javaTypeResolver>
         <javaModelGenerator targetPackage="org.apache.iotdb.mybatis.plugin.model" targetProject="src/main/java">
             <!--            <property name="enableSubPackages" value="true"/>-->
@@ -51,8 +55,11 @@
         <javaClientGenerator type="XMLMAPPER" targetPackage="org.apache.iotdb.mybatis.plugin.mapper" targetProject="src/main/java">
             <!--            <property name="enableSubPackages" value="true"/>-->
         </javaClientGenerator>
-        <table schema="test" tableName="mix" domainObjectName="Mix" enableSelectByPrimaryKey="true" enableInsert="true" enableDeleteByPrimaryKey="true" enableUpdateByPrimaryKey="false">
-            <property name="virtualKeyColumns" value="time,device_id,device_name,device_tag"/>
+        <table schema="test" tableName="mix" domainObjectName="Mix" enableSelectByPrimaryKey="true" enableInsert="true" enableDeleteByPrimaryKey="true" enableUpdateByPrimaryKey="false" enableUpdateByExample="false">
+            <property name="ignoreQualifiersAtRuntime" value="true"/>
+            <property name="virtualKeyColumns" value="time,device_id"/>
+            <columnOverride column="reading_date" javaType="java.time.LocalDate" typeHandler="org.apache.iotdb.mybatis.type.IoTDBLocalDateTypeHandler"/>
+            <columnOverride column="payload" javaType="byte[]" typeHandler="org.apache.iotdb.mybatis.type.IoTDBBlobTypeHandler"/>
         </table>
     </context>
 </generatorConfiguration>
diff --git a/examples/mybatis-generator/src/main/resources/generatorConfigByExample.xml b/examples/mybatis-generator/src/main/resources/generatorConfigByExample.xml
index 7170ceb..253041e 100644
--- a/examples/mybatis-generator/src/main/resources/generatorConfigByExample.xml
+++ b/examples/mybatis-generator/src/main/resources/generatorConfigByExample.xml
@@ -18,9 +18,9 @@
 
 -->
 <generatorConfiguration>
-    <classPathEntry location="/apache/iotdb/iotdb-client/jdbc/target/iotdb-jdbc-2.0.3-jar-with-dependencies.jar"/>
+    <!-- The Maven generator plugin supplies iotdb-jdbc on its plugin classpath. -->
     <!-- mvn mybatis-generator:generate hierarchical/flat-->
-    <context id="mybatis3" targetRuntime="Mybatis3" defaultModelType="flat">
+    <context id="mybatis3" targetRuntime="MyBatis3" defaultModelType="flat">
         <!--        <property name="suppressAllComments" value="true"/>-->
         <!--        <property name="useActualColumnNames" value="false"/>-->
         <!--        <property name="javaFormatter"
@@ -29,6 +29,8 @@
     value="org.mybatis.generator.api.dom.DefaultXmlFormatter"/>-->
         <plugin type="org.apache.iotdb.mybatis.plugin.LombokPlugin"/>
         <plugin type="org.apache.iotdb.mybatis.plugin.BatchInsertPlugin"/>
+        <plugin type="org.apache.iotdb.mybatis.plugin.IoTDBKeyPlugin"/>
+        <plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin"/>
         <!--        <plugin type="org.apache.iotdb.mybatis.plugin.SerializablePlugin"/>-->
         <plugin type="org.mybatis.generator.plugins.VirtualPrimaryKeyPlugin"/>
         <!--        <commentGenerator type="generator.org.apache.iotdb.mybatis.plugin.CNCommentGenerator">-->
@@ -42,6 +44,8 @@
         <jdbcConnection driverClass="org.apache.iotdb.jdbc.IoTDBDriver" connectionURL="jdbc:iotdb://127.0.0.1:6667/test?sql_dialect=table" userId="root" password="root"/>
         <javaTypeResolver type="org.apache.iotdb.mybatis.plugin.generator.resolver.IoTDBJavaTypeResolver">
             <property name="forceBigDecimals" value="false"/>
+            <property name="jdbcType.TIMESTAMP" value="java.lang.Long"/>
+            <property name="jdbcType.FLOAT" value="java.lang.Float"/>
         </javaTypeResolver>
         <javaModelGenerator targetPackage="org.apache.iotdb.mybatis.plugin.model" targetProject="src/main/java">
             <!--            <property name="enableSubPackages" value="true"/>-->
@@ -53,8 +57,11 @@
         <javaClientGenerator type="XMLMAPPER" targetPackage="org.apache.iotdb.mybatis.plugin.mapper" targetProject="src/main/java">
             <!--            <property name="enableSubPackages" value="true"/>-->
         </javaClientGenerator>
-        <table schema="test" tableName="mix" domainObjectName="Mix" enableSelectByPrimaryKey="true" enableInsert="true" enableDeleteByPrimaryKey="true" enableUpdateByPrimaryKey="false" enableSelectByExample="true">
-            <property name="virtualKeyColumns" value="time,device_id,device_name,device_tag"/>
+        <table schema="test" tableName="mix" domainObjectName="Mix" enableSelectByPrimaryKey="true" enableInsert="true" enableDeleteByPrimaryKey="true" enableUpdateByPrimaryKey="false" enableUpdateByExample="false" enableSelectByExample="true">
+            <property name="ignoreQualifiersAtRuntime" value="true"/>
+            <property name="virtualKeyColumns" value="time,device_id"/>
+            <columnOverride column="reading_date" javaType="java.time.LocalDate" typeHandler="org.apache.iotdb.mybatis.type.IoTDBLocalDateTypeHandler"/>
+            <columnOverride column="payload" javaType="byte[]" typeHandler="org.apache.iotdb.mybatis.type.IoTDBBlobTypeHandler"/>
         </table>
     </context>
 </generatorConfiguration>
diff --git a/examples/mybatis-generator/src/main/resources/mybatis-config.xml b/examples/mybatis-generator/src/main/resources/mybatis-config.xml
index 06dfbe9..f8757c4 100644
--- a/examples/mybatis-generator/src/main/resources/mybatis-config.xml
+++ b/examples/mybatis-generator/src/main/resources/mybatis-config.xml
@@ -18,6 +18,9 @@
 
 -->
 <configuration>
+    <plugins>
+        <plugin interceptor="org.apache.iotdb.mybatis.IoTDBQueryInterceptor"/>
+    </plugins>
     <environments default="dev">
         <environment id="dev">
             <transactionManager type="JDBC"/>
diff --git a/examples/mybatis-generator/src/test/java/org/apache/iotdb/mybatis/IoTDBGeneratorIT.java b/examples/mybatis-generator/src/test/java/org/apache/iotdb/mybatis/IoTDBGeneratorIT.java
new file mode 100644
index 0000000..303d864
--- /dev/null
+++ b/examples/mybatis-generator/src/test/java/org/apache/iotdb/mybatis/IoTDBGeneratorIT.java
@@ -0,0 +1,317 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.iotdb.mybatis;
+
+import org.apache.iotdb.mybatis.plugin.BatchInsertPlugin;
+import org.apache.iotdb.mybatis.plugin.IoTDBKeyPlugin;
+import org.apache.iotdb.mybatis.plugin.generator.resolver.IoTDBJavaTypeResolver;
+import org.apache.iotdb.mybatis.plugin.mapper.MixMapper;
+import org.apache.iotdb.mybatis.plugin.model.Mix;
+import org.apache.iotdb.mybatis.type.IoTDBBlobTypeHandler;
+import org.apache.iotdb.mybatis.type.IoTDBLocalDateTypeHandler;
+
+import org.apache.ibatis.builder.xml.XMLMapperBuilder;
+import org.apache.ibatis.datasource.unpooled.UnpooledDataSource;
+import org.apache.ibatis.io.Resources;
+import org.apache.ibatis.mapping.Environment;
+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.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.mybatis.generator.api.MyBatisGenerator;
+import org.mybatis.generator.config.ColumnOverride;
+import org.mybatis.generator.config.Context;
+import org.mybatis.generator.config.JDBCConnectionConfiguration;
+import org.mybatis.generator.config.JavaClientGeneratorConfiguration;
+import org.mybatis.generator.config.JavaModelGeneratorConfiguration;
+import org.mybatis.generator.config.JavaTypeResolverConfiguration;
+import org.mybatis.generator.config.ModelType;
+import org.mybatis.generator.config.PluginConfiguration;
+import org.mybatis.generator.config.SqlMapGeneratorConfiguration;
+import org.mybatis.generator.config.TableConfiguration;
+import org.mybatis.generator.internal.DefaultShellCallback;
+
+import javax.tools.ToolProvider;
+
+import java.io.InputStream;
+import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.Statement;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/** Requires an explicitly selected iotdb-mybatis-it profile and an IoTDB 2.0.11 server. */
+public class IoTDBGeneratorIT {
+  @Rule public TemporaryFolder output = new TemporaryFolder();
+  private final String database = "mbg_it_" + UUID.randomUUID().toString().replace("-", "");
+  private final String url =
+      System.getProperty("iotdb.it.url", "jdbc:iotdb://127.0.0.1:6667/?sql_dialect=table");
+  private final String username = System.getProperty("iotdb.it.username", "root");
+  private final String password = System.getProperty("iotdb.it.password", "root");
+  private Connection admin;
+  private boolean databaseCreated;
+  private UnpooledDataSource source;
+
+  private String databaseUrl() {
+    int start = url.indexOf('/', "jdbc:iotdb://".length());
+    int query = url.indexOf('?', start);
+    if (start < 0 || query < 0 || !url.contains("sql_dialect=table")) {
+      throw new IllegalArgumentException("iotdb.it.url must include /?sql_dialect=table");
+    }
+    return url.substring(0, start + 1) + database + url.substring(query);
+  }
+
+  private long timestamp() {
+    switch (System.getProperty("iotdb.it.precision", "ms")) {
+      case "ms":
+        return 1700000000123L;
+      case "us":
+        return 1700000000123456L;
+      case "ns":
+        return 1700000000123456789L;
+      default:
+        throw new IllegalArgumentException("iotdb.it.precision must be ms, us or ns");
+    }
+  }
+
+  @Before
+  public void prepare() throws Exception {
+    Class.forName("org.apache.iotdb.jdbc.IoTDBDriver");
+    admin = DriverManager.getConnection(url, username, password);
+    try (Statement sql = admin.createStatement()) {
+      sql.execute("CREATE DATABASE " + database);
+      databaseCreated = true;
+      sql.execute("USE " + database);
+      sql.execute(
+          "CREATE TABLE mix (device_id STRING TAG, region STRING ATTRIBUTE, "
+              + "plant_id STRING ATTRIBUTE, model_id STRING ATTRIBUTE, maintenance STRING ATTRIBUTE, "
+              + "temperature FLOAT FIELD, humidity FLOAT FIELD, status BOOLEAN FIELD, "
+              + "arrival_time TIMESTAMP FIELD, reading_date DATE FIELD, payload BLOB FIELD)");
+      sql.execute(
+          "CREATE TABLE readings (region STRING TAG, \"order\" STRING TAG, "
+              + "temperature FLOAT FIELD, reading_date DATE FIELD, payload BLOB FIELD)");
+    }
+    source =
+        new UnpooledDataSource(
+            "org.apache.iotdb.jdbc.IoTDBDriver", databaseUrl(), username, password);
+  }
+
+  @After
+  public void cleanup() throws Exception {
+    if (admin != null) {
+      try (Connection connection = admin;
+          Statement sql = connection.createStatement()) {
+        if (databaseCreated) sql.execute("DROP DATABASE " + database);
+      }
+    }
+  }
+
+  private SqlSessionFactory factory(InputStream xml, String resource) {
+    org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration();
+    config.setEnvironment(new Environment("it", new JdbcTransactionFactory(), source));
+    config.addInterceptor(new IoTDBQueryInterceptor());
+    new XMLMapperBuilder(xml, config, resource, config.getSqlFragments()).parse();
+    return new SqlSessionFactoryBuilder().build(config);
+  }
+
+  @Test
+  public void checkedInMapperRoundTripsRawTimeDateBlobAndNullKeys() throws Exception {
+    try (InputStream xml =
+            Resources.getResourceAsStream("org/apache/iotdb/mybatis/plugin/xml/MixMapper.xml");
+        SqlSession session = factory(xml, "MixMapper.xml").openSession(true)) {
+      MixMapper mapper = session.getMapper(MixMapper.class);
+      Mix first = new Mix();
+      first.setTime(timestamp());
+      first.setDeviceId("d");
+      first.setTemperature(1.5f);
+      first.setArrivalTime(timestamp() + 1);
+      first.setReadingDate(LocalDate.of(2024, 2, 29));
+      first.setPayload(new byte[] {0, 39, 92, (byte) 128, (byte) 255});
+      Mix nullTag = new Mix();
+      nullTag.setTime(first.getTime());
+      nullTag.setTemperature(2.5f);
+      assertEquals(0, mapper.batchInsert(List.of()));
+      mapper.batchInsert(List.of(first, nullTag));
+      Mix actual = mapper.selectByPrimaryKey(first.getTime(), "d");
+      assertEquals(first.getTime(), actual.getTime());
+      assertEquals(first.getArrivalTime(), actual.getArrivalTime());
+      assertEquals(first.getReadingDate(), actual.getReadingDate());
+      assertArrayEquals(first.getPayload(), actual.getPayload());
+      assertEquals(Float.valueOf(1.5f), actual.getTemperature());
+      actual = mapper.selectByPrimaryKey(first.getTime(), null);
+      assertEquals(Float.valueOf(2.5f), actual.getTemperature());
+      assertNull(actual.getPayload());
+      assertNull(actual.getReadingDate());
+      mapper.deleteByPrimaryKey(first.getTime(), null);
+      assertNull(mapper.selectByPrimaryKey(first.getTime(), null));
+      assertNotNull(mapper.selectByPrimaryKey(first.getTime(), "d"));
+    }
+  }
+
+  @Test
+  public void actualMetadataGeneratesCompilableMappersAndChunkedInserts() throws Exception {
+    Path generated = output.newFolder("generated").toPath();
+    org.mybatis.generator.config.Configuration configuration =
+        new org.mybatis.generator.config.Configuration();
+    Context context = new Context(ModelType.FLAT);
+    context.setId("iotdb");
+    context.setTargetRuntime("MyBatis3Simple");
+    context.addProperty("beginningDelimiter", "\"");
+    context.addProperty("endingDelimiter", "\"");
+    JDBCConnectionConfiguration jdbc = new JDBCConnectionConfiguration();
+    jdbc.setDriverClass("org.apache.iotdb.jdbc.IoTDBDriver");
+    jdbc.setConnectionURL(databaseUrl());
+    jdbc.setUserId(username);
+    jdbc.setPassword(password);
+    context.setJdbcConnectionConfiguration(jdbc);
+    JavaTypeResolverConfiguration resolver = new JavaTypeResolverConfiguration();
+    resolver.setConfigurationType(IoTDBJavaTypeResolver.class.getName());
+    resolver.addProperty("jdbcType.FLOAT", "java.lang.Float");
+    context.setJavaTypeResolverConfiguration(resolver);
+    for (Class<?> plugin :
+        List.of(
+            BatchInsertPlugin.class,
+            IoTDBKeyPlugin.class,
+            org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin.class,
+            org.mybatis.generator.plugins.VirtualPrimaryKeyPlugin.class)) {
+      PluginConfiguration pc = new PluginConfiguration();
+      pc.setConfigurationType(plugin.getName());
+      pc.addProperty("batchSize", "2");
+      context.addPluginConfiguration(pc);
+    }
+    JavaModelGeneratorConfiguration models = new JavaModelGeneratorConfiguration();
+    models.setTargetProject(generated.toString());
+    models.setTargetPackage("generated");
+    context.setJavaModelGeneratorConfiguration(models);
+    JavaClientGeneratorConfiguration clients = new JavaClientGeneratorConfiguration();
+    clients.setTargetProject(generated.toString());
+    clients.setTargetPackage("generated");
+    clients.setConfigurationType("XMLMAPPER");
+    context.setJavaClientGeneratorConfiguration(clients);
+    SqlMapGeneratorConfiguration mappings = new SqlMapGeneratorConfiguration();
+    mappings.setTargetProject(generated.toString());
+    mappings.setTargetPackage("generated");
+    context.setSqlMapGeneratorConfiguration(mappings);
+    TableConfiguration table = new TableConfiguration(context);
+    table.setSchema(database);
+    table.setTableName("readings");
+    table.setDomainObjectName("Reading");
+    table.addProperty("virtualKeyColumns", "time,region,order");
+    table.setDelimitIdentifiers(true);
+    table.setAllColumnDelimitingEnabled(true);
+    table.setUpdateByExampleStatementEnabled(false);
+    table.setUpdateByPrimaryKeyStatementEnabled(false);
+    ColumnOverride date = new ColumnOverride("reading_date");
+    date.setJavaType("java.time.LocalDate");
+    date.setTypeHandler(IoTDBLocalDateTypeHandler.class.getName());
+    table.addColumnOverride(date);
+    ColumnOverride blob = new ColumnOverride("payload");
+    blob.setJavaType("byte[]");
+    blob.setTypeHandler(IoTDBBlobTypeHandler.class.getName());
+    table.addColumnOverride(blob);
+    context.addTableConfiguration(table);
+    configuration.addContext(context);
+    List<String> warnings = new ArrayList<>();
+    new MyBatisGenerator(configuration, new DefaultShellCallback(true), warnings).generate(null);
+    assertTrue(warnings.toString(), warnings.isEmpty());
+
+    Path classes = output.newFolder("classes").toPath();
+    List<String> args =
+        new ArrayList<>(
+            List.of(
+                "-proc:none",
+                "-classpath",
+                System.getProperty("java.class.path"),
+                "-d",
+                classes.toString()));
+    try (java.util.stream.Stream<Path> files = Files.walk(generated)) {
+      args.addAll(
+          files
+              .filter(p -> p.toString().endsWith(".java"))
+              .map(Path::toString)
+              .collect(Collectors.toList()));
+    }
+    assertEquals(
+        0, ToolProvider.getSystemJavaCompiler().run(null, null, null, args.toArray(new String[0])));
+    ClassLoader previous = Resources.getDefaultClassLoader();
+    try (URLClassLoader loader =
+        new URLClassLoader(
+            new java.net.URL[] {classes.toUri().toURL()}, getClass().getClassLoader())) {
+      Resources.setDefaultClassLoader(loader);
+      Class<?> entity = loader.loadClass("generated.Reading");
+      Class<?> mapperType = loader.loadClass("generated.ReadingMapper");
+      try (InputStream xml =
+              Files.newInputStream(generated.resolve("generated/ReadingMapper.xml"));
+          SqlSession session = factory(xml, "ReadingMapper.xml").openSession(true)) {
+        org.junit.Assert.assertFalse(
+            session.getConfiguration().hasStatement("generated.ReadingMapper.updateByPrimaryKey"));
+        List<Object> rows = new ArrayList<>();
+        for (int i = 0; i < 5; i++) {
+          Object row = entity.getConstructor().newInstance();
+          entity.getMethod("setTime", Long.class).invoke(row, timestamp() + i);
+          entity.getMethod("setOrder", String.class).invoke(row, "o");
+          entity.getMethod("setTemperature", Float.class).invoke(row, 12.5f);
+          entity
+              .getMethod("setReadingDate", LocalDate.class)
+              .invoke(row, LocalDate.of(2024, 2, 29));
+          entity
+              .getMethod("setPayload", byte[].class)
+              .invoke(row, (Object) new byte[] {0, (byte) 255});
+          rows.add(row);
+        }
+        Object mapper = session.getMapper(mapperType);
+        mapperType.getMethod("batchInsert", List.class).invoke(mapper, rows);
+        Map<String, Object> key = new HashMap<>();
+        key.put("time", timestamp());
+        key.put("region", null);
+        key.put("order", "o");
+        Object actual = session.selectOne("generated.ReadingMapper.selectByPrimaryKey", key);
+        assertNotNull(actual);
+        assertEquals(timestamp(), entity.getMethod("getTime").invoke(actual));
+        assertEquals(LocalDate.of(2024, 2, 29), entity.getMethod("getReadingDate").invoke(actual));
+        assertArrayEquals(
+            new byte[] {0, (byte) 255}, (byte[]) entity.getMethod("getPayload").invoke(actual));
+        assertEquals(5, session.selectList("generated.ReadingMapper.selectAll").size());
+        session.delete("generated.ReadingMapper.deleteByPrimaryKey", key);
+        assertNull(session.selectOne("generated.ReadingMapper.selectByPrimaryKey", key));
+        assertEquals(4, session.selectList("generated.ReadingMapper.selectAll").size());
+      }
+    } finally {
+      Resources.setDefaultClassLoader(previous);
+    }
+  }
+}
diff --git a/examples/mybatis-generator/src/test/java/org/apache/iotdb/mybatis/MapperConfigurationTest.java b/examples/mybatis-generator/src/test/java/org/apache/iotdb/mybatis/MapperConfigurationTest.java
new file mode 100644
index 0000000..f7e0e9a
--- /dev/null
+++ b/examples/mybatis-generator/src/test/java/org/apache/iotdb/mybatis/MapperConfigurationTest.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.iotdb.mybatis;
+
+import org.apache.iotdb.mybatis.plugin.mapper.MixMapper;
+import org.apache.iotdb.mybatis.type.IoTDBBlobTypeHandler;
+
+import org.apache.ibatis.io.Resources;
+import org.apache.ibatis.mapping.BoundSql;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.apache.ibatis.session.SqlSessionFactoryBuilder;
+import org.junit.Test;
+
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class MapperConfigurationTest {
+  @Test
+  public void packagedConfigurationParsesAndUsesNullableCompositeKey() throws Exception {
+    try (InputStream input = Resources.getResourceAsStream("mybatis-config.xml")) {
+      SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(input);
+      assertTrue(
+          factory.getConfiguration().getInterceptors().stream()
+              .anyMatch(IoTDBQueryInterceptor.class::isInstance));
+      Map<String, Object> key = new HashMap<>();
+      key.put("time", 1700000000123456789L);
+      key.put("deviceId", null);
+      for (String operation : new String[] {"selectByPrimaryKey", "deleteByPrimaryKey"}) {
+        BoundSql sql =
+            factory
+                .getConfiguration()
+                .getMappedStatement(MixMapper.class.getName() + "." + operation)
+                .getBoundSql(key);
+        assertTrue(sql.getSql().contains("\"device_id\" IS NULL"));
+        assertEquals(1, sql.getParameterMappings().size());
+      }
+      assertTrue(
+          factory
+              .getConfiguration()
+              .getResultMap(MixMapper.class.getName() + ".BaseResultMap")
+              .getResultMappings()
+              .stream()
+              .anyMatch(mapping -> mapping.getTypeHandler() instanceof IoTDBBlobTypeHandler));
+    }
+  }
+}
diff --git a/examples/mybatisplus-generator/README.md b/examples/mybatisplus-generator/README.md
index 4314992..a694794 100644
--- a/examples/mybatisplus-generator/README.md
+++ b/examples/mybatisplus-generator/README.md
@@ -18,164 +18,90 @@
     under the License.
 
 -->
-# MybatisPlus-Generator Demo
-## Introduction
 
-This demo shows how to use IoTDB-MybatisPlus-Generator
+# MyBatis-Plus example
 
-### Version usage
+An IoTDB **table-model** example using **JDK 17+, MyBatis-Plus 3.5.15 and JDBC 2.0.11**. The default build uses Spring Boot 3.5.1; `spring-boot4` selects Boot 4.1.1 and `mybatis-plus-spring-boot4-starter`. Each build imports one Boot BOM; the module inherits the reactor parent, so the reactor's Spring, Jackson and Mockito management is re-pointed at the selected Boot line in the pom, and the reactor quality gates (Spotless, Checkstyle, RAT) run on it like on every other module. Velocity  [...]
 
-IoTDB: 2.0.1-beta
-mybatisPlus: 3.5.10
+## Prepare and run
 
-### 1. Install IoTDB
+Create database `database1`, then create both `table1` and `table2` with this schema (replace the table name for the second table):
 
-please refer to [https://iotdb.apache.org/#/Download](https://iotdb.apache.org/#/Download)
+```sql
+CREATE DATABASE IF NOT EXISTS database1;
+USE database1;
+CREATE TABLE IF NOT EXISTS table1 (
+    region STRING TAG, plant_id STRING TAG, device_id STRING TAG,
+    model_id STRING ATTRIBUTE, maintenance STRING ATTRIBUTE,
+    temperature FLOAT FIELD, humidity FLOAT FIELD, status BOOLEAN FIELD,
+    arrival_time TIMESTAMP FIELD, reading_date DATE FIELD, payload BLOB FIELD
+);
+```
 
-### 2. Startup IoTDB
+Add the DATE/BLOB columns separately when migrating an existing table; CREATE IF NOT EXISTS does not change it.
 
-please refer to [Quick Start](http://iotdb.apache.org/UserGuide/Master/Get%20Started/QuickStart.html)
+Configure `src/main/resources/application.yml`, then run `org.apache.iotdb.Main` from the IDE. Startup does not run code generation. Mapper XML resides under **src/main/resources/mappers**, so it is packaged in the JAR. `IoTDBMybatisConfiguration` registers the query interceptor and the restricted SQL injector.
 
-Then we need to create a database 'test' by cli in table model
-```
-create database test;
-use test;
-```
-Then we need to create a database 'table'
-```sql
-CREATE TABLE mix (
-    time TIMESTAMP TIME,
-    region STRING TAG,
-    plant_id STRING TAG,
-    device_id STRING TAG,
-    model_id STRING ATTRIBUTE,
-    maintenance STRING ATTRIBUTE,
-    temperature FLOAT FIELD,
-    humidity FLOAT FIELD,
-    status Boolean FIELD,
-    arrival_time TIMESTAMP FIELD
-) WITH (TTL=31536000000);
+Build/test from the repository root:
+
+```sh
+mvn -Pwith-examples,with-springboot -pl examples/mybatisplus-generator -am clean verify
+mvn -Pwith-examples,with-springboot,spring-boot4 -pl examples/mybatisplus-generator -am clean verify
 ```
 
-### 3. Build Dependencies with Maven in your Project
+## Supported operations
 
-```
-<properties>
-    <mybatisplus.version>3.5.10</mybatisplus.version>
-    <maven.compiler.source>17</maven.compiler.source>
-    <maven.compiler.target>17</maven.compiler.target>
-    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-    <spring-boot.version>3.4.5</spring-boot.version>
-    <spring.version>6.2.6</spring.version>
-    <iotdb-jdbc.version>2.0.5</iotdb-jdbc.version>
-    <io-springfox.version>3.0.0</io-springfox.version>
-</properties>
-
-<dependencies>
-    <dependency>
-        <groupId>com.baomidou</groupId>
-        <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
-        <version>${mybatisplus.version}</version>
-    </dependency>
-    <dependency>
-        <groupId>com.baomidou</groupId>
-        <artifactId>mybatis-plus-generator</artifactId>
-        <version>${mybatisplus.version}</version>
-    </dependency>
-
-    <dependency>
-        <groupId>org.apache.iotdb</groupId>
-        <artifactId>iotdb-jdbc</artifactId>
-        <version>${iotdb-jdbc.version}</version>
-    </dependency>
-    <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter</artifactId>
-        <version>${spring-boot.version}</version>
-    </dependency>
-    <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-web</artifactId>
-        <version>${spring-boot.version}</version>
-    </dependency>
-    <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-test</artifactId>
-        <version>${spring-boot.version}</version>
-        <scope>test</scope>
-    </dependency>
-
-    <dependency>
-        <groupId>io.springfox</groupId>
-        <artifactId>springfox-swagger2</artifactId>
-        <version>${io-springfox.version}</version>
-    </dependency>
-    <dependency>
-        <groupId>io.springfox</groupId>
-        <artifactId>springfox-swagger-ui</artifactId>
-        <version>${io-springfox.version}</version>
-    </dependency>
-
-    <dependency>
-        <groupId>org.projectlombok</groupId>
-        <artifactId>lombok</artifactId>
-        <version>1.18.36</version>
-    </dependency>
-    <dependency>
-        <groupId>com.github.jeffreyning</groupId>
-        <artifactId>mybatisplus-plus</artifactId>
-        <version>1.7.5-RELEASE</version>
-    </dependency>
-</dependencies>
-```
+TIME plus **all TAG columns** identifies a row. The example uses `IoTDBTableMapper<T>` and explicit XML/service APIs. It no longer relies on `@MppMultiId`, `BaseMapper.*ById` or `IService.update`: those APIs do not establish IoTDB composite-key or FIELD-update semantics.
 
-### 4. Start the Main.java
+| Service operation | Behavior |
+|---|---|
+| `insert(row)` | Requires time; inserts the supplied values |
+| `selectByKey(key)` | TIME + region + plantId + deviceId; NULL TAGs use IS NULL |
+| `deleteByKey(key)` | Same full key; see the high-precision server limitation below |
+| `upsertFields(row)` | Requires time and at least one non-null FIELD; INSERTs only the supplied FIELDs under that key |
+| `updateAttributes(row)` | Updates modelId and maintenance for the TAG-defined device, across all timestamps; null clears an attribute |
+| `list()` | Ordered query capped at 1000 rows |
 
-### 5. the target file location
+`upsertFields` leaves omitted/null FIELDs unchanged and ignores ATTRIBUTE properties. `updateAttributes` writes **both** attribute properties, including null; it does not use time. Always supply the intended device TAGs. Null TAG components identify the null-valued device, not a wildcard.
 
-You can see the target file in your Project
-```
-org/apache/iotdb/controller/MixController.java
-org/apache/iotdb/entity/Mix.java
-org/apache/iotdb/mapper/MixMapper.xml
-org/apache/iotdb/service/MixService.java  
-org/apache/iotdb/service/MixServiceImpl.java
-org/apache/iotdb/MixMapper.xml
+Use the services when you need their input validation. Generated mappers are lower-level SQL APIs; calls to them must supply a valid key and at least one FIELD for a field patch. Custom query wrappers should also include bounded time/device filters. The injector retains MyBatis-Plus's ordinary behavior for unrelated mapper types; in a multi-database app use separate SqlSessionFactory configurations.
 
-```
+TIME/TIMESTAMP use **Long raw ticks in server precision**, FLOAT uses Float, DATE uses LocalDate, and BLOB uses byte[]. `autoResultMap`, field annotations and XML preserve the [runtime handlers](../../mybatis-support/README.md). Applications migrating from Date timestamps must update their conversions and callers.
+
+Write methods return the JDBC update count, which can be **-1 (unknown)** after success. Failures raise exceptions. IoTDB JDBC 2.0.11 does not implement rollback; `@Transactional` and multiple inserts are not an atomic transaction.
+
+## Explicit code generation
 
-### 6. add & alter Annotations
+Run `org.apache.iotdb.CodeGenerator` from the IDE with these optional JVM properties:
 
-The generated code files `entity/Table1.java` and `entity/Table2.java` need to be manually adjusted to support multi-primary key queries.
+| Property | Default |
+|---|---|
+| `iotdb.url` | `jdbc:iotdb://127.0.0.1:6667/database1?sql_dialect=table` |
+| `iotdb.database` | `database1` |
+| `iotdb.username` / `iotdb.password` | root / root |
+| `iotdb.output` | `target/generated-iotdb` |
 
-```java
-// add import
-import com.github.jeffreyning.mybatisplus.anno.MppMultiId;
+Program arguments select tables, defaulting to `table1 table2`. The URL's selected database must match `iotdb.database`.
 
-// add @MppMultiId
-@MppMultiId
-// alter @TableId() -->> @TableField()
-@TableField("time")
-private Date time;
+The generator reads actual column **TIME/TAG/ATTRIBUTE/FIELD** categories with DESC and renders custom Velocity templates. Java files go to `target/generated-iotdb/java`, XML to `target/generated-iotdb/resources/mappers`. It overwrites files in that output directory on reruns; review and copy the entity, mapper and XML together into source/resources. Service/controller generation is disabled so regeneration preserves the handwritten validation APIs.
 
-// add @MppMultiId
-@MppMultiId
-// alter @TableId() -->> @TableField()
-@TableField("region")
-private String region;
+Generated mappings retain all key columns, nullable TAGs, identifier quoting and DATE/BLOB handlers. FIELD patches remain INSERT; UPDATE is generated only for ATTRIBUTE columns. Tables without attributes expose an unsupported `updateAttributes` default method. Invalid or colliding Java property names fail generation and require an explicit naming adaptation.
 
-// add @MppMultiId
-@MppMultiId
-// alter @TableId() -->> @TableField()
-@TableField("plant_id")
-private String plantId;
+## Tests
 
-// add @MppMultiId
-@MppMultiId
-// alter @TableId() -->> @TableField()
-@TableField("device_id")
-private String deviceId;
-//
+Offline tests start the Spring context without a database, inspect both tables' mapped SQL, check validation and compile generated Java templates.
 
+Opt into live generation/compilation/CRUD:
+
+```sh
+# Repository root; use a test server/account with CREATE/DROP DATABASE permission.
+mvn -Pwith-examples,with-springboot,iotdb-mybatis-it -pl examples/mybatisplus-generator -am verify \
+  '-Diotdb.it.url=jdbc:iotdb://127.0.0.1:6667/?sql_dialect=table' \
+  -Diotdb.it.precision=ms
+
+# Add spring-boot4 to the profile list to exercise Boot 4 against the same server.
 ```
 
+`iotdb.it.username/password` default to root/root. `iotdb.it.precision` accepts ms/us/ns and must match the server. The suite fails when the server is unavailable; it creates and cleans up only its unique test database. It covers colliding timestamps across TAGs, NULL TAGs, FIELD patches, device attributes, DATE/BLOB/NULL, real metadata generation and compiled mapper execution.
+
+The **ms** suite passes on official IoTDB 2.0.11 with Boot 3 and Boot 4. On servers configured with **us/ns precision**, a DELETE with a time predicate can report success without deleting the matching row. This server routing issue also reproduces with raw JDBC and causes the strict key-deletion assertions to fail. Reads and writes preserve raw Long timestamp precision, but key deletion on us/ns servers requires a server-side fix. Do not remove the time predicate as a workaround: doing s [...]
diff --git a/examples/mybatisplus-generator/pom.xml b/examples/mybatisplus-generator/pom.xml
index cf2b14c..26bb891 100644
--- a/examples/mybatisplus-generator/pom.xml
+++ b/examples/mybatisplus-generator/pom.xml
@@ -22,31 +22,67 @@
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
     <parent>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-parent</artifactId>
-        <version>3.5.1</version>
-        <relativePath/>
-        <!-- lookup parent from repository -->
+        <groupId>org.apache.iotdb</groupId>
+        <artifactId>examples</artifactId>
+        <version>2.0.4-SNAPSHOT</version>
     </parent>
-    <groupId>org.apache.iotdb</groupId>
     <artifactId>mybatisplus-generator-example</artifactId>
     <name>IoTDB: Example: Mybatis Plus Generator</name>
     <version>2.0.4-SNAPSHOT</version>
     <properties>
-        <mybatisplus.version>3.5.10</mybatisplus.version>
-        <maven.compiler.source>17</maven.compiler.source>
-        <maven.compiler.target>17</maven.compiler.target>
+        <mybatisplus.version>3.5.15</mybatisplus.version>
+        <mybatisplus-starter>mybatis-plus-spring-boot3-starter</mybatisplus-starter>
+        <web-starter>spring-boot-starter-web</web-starter>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <!--
+          The reactor parent manages spring-boot, spring-boot-autoconfigure, the spring-* artifacts,
+          jackson-bom and mockito-core for the legacy Boot 2 / Spring 5 Grafana WAR. Inherited
+          management wins over the BOM imported below, so those lines are re-pointed at the
+          Spring Boot 3.5.1 values here and at the Spring Boot 4.1.1 values in the spring-boot4
+          profile. Logging (slf4j, logback) stays on the reactor's line.
+        -->
         <spring-boot.version>3.5.1</spring-boot.version>
-        <iotdb-jdbc.version>2.0.5</iotdb-jdbc.version>
-        <io-springfox.version>3.0.0</io-springfox.version>
+        <spring.version>6.2.8</spring.version>
+        <jackson.version>2.19.1</jackson.version>
+        <jackson-annotations.version>2.19.1</jackson-annotations.version>
+        <mockito.version>5.17.0</mockito.version>
     </properties>
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-dependencies</artifactId>
+                <version>${spring-boot.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <!--
+              Jackson 2.21 publishes jackson-annotations as 2.21 while the rest of the line is
+              2.21.x; the reactor pins jackson-annotations to ${jackson.version}, so re-pin it here.
+            -->
+            <dependency>
+                <groupId>com.fasterxml.jackson.core</groupId>
+                <artifactId>jackson-annotations</artifactId>
+                <version>${jackson-annotations.version}</version>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
     <dependencies>
+        <dependency>
+            <groupId>org.apache.iotdb</groupId>
+            <artifactId>mybatis-support</artifactId>
+            <version>${project.version}</version>
+        </dependency>
         <dependency>
             <groupId>com.baomidou</groupId>
-            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
+            <artifactId>${mybatisplus-starter}</artifactId>
             <version>${mybatisplus.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.velocity</groupId>
+            <artifactId>velocity-engine-core</artifactId>
+            <version>2.4.1</version>
+        </dependency>
         <dependency>
             <groupId>com.baomidou</groupId>
             <artifactId>mybatis-plus-generator</artifactId>
@@ -55,7 +91,6 @@
         <dependency>
             <groupId>org.apache.iotdb</groupId>
             <artifactId>iotdb-jdbc</artifactId>
-            <version>${iotdb-jdbc.version}</version>
             <exclusions>
                 <exclusion>
                     <groupId>commons-logging</groupId>
@@ -66,37 +101,20 @@
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter</artifactId>
-            <version>${spring-boot.version}</version>
         </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-web</artifactId>
+            <artifactId>${web-starter}</artifactId>
         </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-test</artifactId>
-            <version>${spring-boot.version}</version>
             <scope>test</scope>
         </dependency>
-        <dependency>
-            <groupId>io.springfox</groupId>
-            <artifactId>springfox-swagger2</artifactId>
-            <version>${io-springfox.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>io.springfox</groupId>
-            <artifactId>springfox-swagger-ui</artifactId>
-            <version>${io-springfox.version}</version>
-        </dependency>
         <dependency>
             <groupId>org.projectlombok</groupId>
             <artifactId>lombok</artifactId>
-            <version>1.18.36</version>
-        </dependency>
-        <dependency>
-            <groupId>com.github.jeffreyning</groupId>
-            <artifactId>mybatisplus-plus</artifactId>
-            <version>1.7.5-RELEASE</version>
+            <optional>true</optional>
         </dependency>
         <dependency>
             <groupId>ch.qos.logback</groupId>
@@ -110,27 +128,63 @@
     <build>
         <plugins>
             <plugin>
-                <groupId>org.mybatis.generator</groupId>
-                <artifactId>mybatis-generator-maven-plugin</artifactId>
-                <version>1.4.2</version>
-                <dependencies>
-                    <dependency>
-                        <groupId>org.apache.iotdb</groupId>
-                        <artifactId>mybatis-generator-plugin</artifactId>
-                        <version>2.0.4-SNAPSHOT</version>
-                    </dependency>
-                </dependencies>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
                 <configuration>
-                    <verbose>true</verbose>
-                    <overwrite>true</overwrite>
-                    <configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
+                    <parameters>true</parameters>
                 </configuration>
             </plugin>
             <plugin>
-                <groupId>com.diffplug.spotless</groupId>
-                <artifactId>spotless-maven-plugin</artifactId>
-                <version>2.43.0</version>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <!-- JUnit Platform 6 (Spring Boot 4) needs a newer provider than the reactor's 3.1.2. -->
+                <version>3.5.3</version>
+                <executions>
+                    <!-- *IT classes need a live server; they run through failsafe under iotdb-mybatis-it. -->
+                    <execution>
+                        <id>integration-tests</id>
+                        <phase>none</phase>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-failsafe-plugin</artifactId>
+                <version>3.5.3</version>
             </plugin>
         </plugins>
     </build>
+    <profiles>
+        <profile>
+            <id>spring-boot4</id>
+            <properties>
+                <spring-boot.version>4.1.1</spring-boot.version>
+                <spring.version>7.0.9</spring.version>
+                <jackson.version>2.21.5</jackson.version>
+                <jackson-annotations.version>2.21</jackson-annotations.version>
+                <mockito.version>5.23.0</mockito.version>
+                <mybatisplus-starter>mybatis-plus-spring-boot4-starter</mybatisplus-starter>
+                <web-starter>spring-boot-starter-webmvc</web-starter>
+            </properties>
+        </profile>
+        <profile>
+            <id>iotdb-mybatis-it</id>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-failsafe-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <goals>
+                                    <goal>integration-test</goal>
+                                    <goal>verify</goal>
+                                </goals>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
 </project>
diff --git a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/CodeGenerator.java b/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/CodeGenerator.java
new file mode 100644
index 0000000..7c58558
--- /dev/null
+++ b/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/CodeGenerator.java
@@ -0,0 +1,351 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.iotdb;
+
+import org.apache.iotdb.jdbc.IoTDBDataSource;
+
+import com.baomidou.mybatisplus.generator.FastAutoGenerator;
+import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
+import com.baomidou.mybatisplus.generator.config.OutputFile;
+import com.baomidou.mybatisplus.generator.config.rules.DbColumnType;
+import com.baomidou.mybatisplus.generator.engine.VelocityTemplateEngine;
+
+import javax.lang.model.SourceVersion;
+import javax.sql.DataSource;
+
+import java.nio.file.Path;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.Types;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/** Explicit development tool. Review target/generated-iotdb before copying generated files. */
+public class CodeGenerator {
+  public static void main(String[] args) throws SQLException {
+    String database = System.getProperty("iotdb.database", "database1");
+    IoTDBDataSource dataSource = new IoTDBDataSource();
+    dataSource.setUrl(
+        System.getProperty(
+            "iotdb.url", "jdbc:iotdb://127.0.0.1:6667/" + database + "?sql_dialect=table"));
+    dataSource.setUser(System.getProperty("iotdb.username", "root"));
+    dataSource.setPassword(System.getProperty("iotdb.password", "root"));
+    generate(
+        dataSource,
+        database,
+        List.of(args.length == 0 ? new String[] {"table1", "table2"} : args),
+        Path.of(System.getProperty("iotdb.output", "target/generated-iotdb")));
+  }
+
+  public static void generate(DataSource source, String database, List<String> tables, Path output)
+      throws SQLException {
+    Map<String, Map<String, Object>> schemas = new LinkedHashMap<>();
+    try (Connection connection = source.getConnection()) {
+      for (String table : tables) {
+        List<Column> columns = new ArrayList<>();
+        try (Statement statement = connection.createStatement();
+            ResultSet result =
+                statement.executeQuery("DESC " + quote(database) + "." + quote(table))) {
+          while (result.next()) {
+            columns.add(
+                new Column(
+                    result.getString("ColumnName"),
+                    result.getString("DataType"),
+                    result.getString("Category")));
+          }
+        }
+        schemas.put(table, templateData(columns));
+      }
+    }
+    FastAutoGenerator.create(new DataSourceConfig.Builder(source))
+        .globalConfig(
+            builder ->
+                builder
+                    .author("IoTDB")
+                    .disableOpenDir()
+                    .outputDir(output.resolve("java").toString()))
+        .packageConfig(
+            builder ->
+                builder
+                    .parent("org.apache.iotdb")
+                    .pathInfo(
+                        Map.of(OutputFile.xml, output.resolve("resources/mappers").toString())))
+        .dataSourceConfig(
+            builder ->
+                builder.typeConvertHandler(
+                    (global, registry, info) -> {
+                      switch (info.getJdbcType().TYPE_CODE) {
+                        case Types.TIMESTAMP:
+                          return DbColumnType.LONG;
+                        case Types.FLOAT:
+                          return DbColumnType.FLOAT;
+                        default:
+                          return registry.getColumnType(info);
+                      }
+                    }))
+        .strategyConfig(
+            builder -> {
+              builder.addInclude(tables);
+              builder
+                  .entityBuilder()
+                  .javaTemplate("/templates/iotdb-entity.java.vm")
+                  .enableFileOverride();
+              builder
+                  .mapperBuilder()
+                  .mapperTemplate("/templates/iotdb-mapper.java.vm")
+                  .mapperXmlTemplate("/templates/iotdb-mapper.xml.vm")
+                  .enableFileOverride();
+              builder.serviceBuilder().disable();
+              builder.controllerBuilder().disable();
+            })
+        .injectionConfig(
+            builder ->
+                builder.beforeOutputFile(
+                    (table, data) -> {
+                      Map<String, Object> schema = schemas.get(table.getName());
+                      if (schema == null)
+                        throw new IllegalArgumentException(
+                            "Missing IoTDB schema: " + table.getName());
+                      data.putAll(schema);
+                      data.put("iotdbTable", xml(quote(table.getName())));
+                      data.put("iotdbJavaTable", javaString(quote(table.getName())));
+                    }))
+        .templateEngine(new VelocityTemplateEngine())
+        .execute();
+  }
+
+  static Map<String, Object> templateData(List<Column> columns) {
+    java.util.Set<String> properties = new HashSet<>();
+    for (Column column : columns) {
+      String property = column.property();
+      if (!SourceVersion.isIdentifier(property)
+          || SourceVersion.isKeyword(property)
+          || !properties.add(property)) {
+        throw new IllegalArgumentException(
+            "Column needs an explicit Java property mapping: " + column.name);
+      }
+    }
+    List<Column> keys = columns.stream().filter(Column::key).collect(Collectors.toList());
+    List<Column> tags =
+        columns.stream().filter(c -> c.category.equals("TAG")).collect(Collectors.toList());
+    List<Column> fields =
+        columns.stream().filter(c -> c.category.equals("FIELD")).collect(Collectors.toList());
+    List<Column> attributes =
+        columns.stream().filter(c -> c.category.equals("ATTRIBUTE")).collect(Collectors.toList());
+    if (keys.stream().noneMatch(c -> c.category.equals("TIME"))) {
+      throw new IllegalArgumentException("IoTDB TIME column is required");
+    }
+    Map<String, Object> data = new LinkedHashMap<>();
+    data.put(
+        "iotdbFields",
+        columns.stream()
+            .map(
+                c ->
+                    "  @com.baomidou.mybatisplus.annotation.TableField(value = "
+                        + javaString(quote(c.name))
+                        + (c.handler() == null ? "" : ", typeHandler = " + c.handler() + ".class")
+                        + ")\n"
+                        + "  private "
+                        + c.javaType()
+                        + " "
+                        + c.property()
+                        + ";")
+            .collect(Collectors.joining("\n\n")));
+    data.put(
+        "iotdbResults",
+        columns.stream()
+            .sorted(Comparator.comparing(c -> !c.key()))
+            .map(
+                c ->
+                    "    <"
+                        + (c.key() ? "id" : "result")
+                        + " column=\""
+                        + xml(c.name)
+                        + "\" property=\""
+                        + c.property()
+                        + "\" jdbcType=\""
+                        + c.jdbcType()
+                        + "\""
+                        + (c.handler() == null ? "" : " typeHandler=\"" + c.handler() + "\"")
+                        + "/>")
+            .collect(Collectors.joining("\n")));
+    data.put(
+        "iotdbColumns",
+        columns.stream().map(c -> xml(quote(c.name))).collect(Collectors.joining(", ")));
+    data.put(
+        "iotdbKeyPredicate",
+        keys.stream().map(c -> predicate(c, "key")).collect(Collectors.joining("\n")));
+    data.put(
+        "iotdbTagPredicate",
+        tags.stream().map(c -> predicate(c, "row")).collect(Collectors.joining("\n")));
+    // A table without TAGs represents one device.
+    if (tags.isEmpty()) data.put("iotdbTagPredicate", "1 = 1");
+    data.put(
+        "iotdbUpsertColumns",
+        keys.stream().map(c -> xml(quote(c.name)) + ",").collect(Collectors.joining("\n"))
+            + "\n"
+            + fields.stream()
+                .map(
+                    c ->
+                        "<if test=\"row."
+                            + c.property()
+                            + " != null\">"
+                            + xml(quote(c.name))
+                            + ",</if>")
+                .collect(Collectors.joining("\n")));
+    data.put(
+        "iotdbUpsertValues",
+        keys.stream().map(c -> c.parameter("row") + ",").collect(Collectors.joining("\n"))
+            + "\n"
+            + fields.stream()
+                .map(
+                    c ->
+                        "<if test=\"row."
+                            + c.property()
+                            + " != null\">"
+                            + c.parameter("row")
+                            + ",</if>")
+                .collect(Collectors.joining("\n")));
+    data.put(
+        "iotdbAttributes",
+        attributes.stream()
+            .map(c -> xml(quote(c.name)) + " = " + c.parameter("row"))
+            .collect(Collectors.joining(", ")));
+    data.put("iotdbHasAttributes", !attributes.isEmpty());
+    return data;
+  }
+
+  private static String predicate(Column c, String parameter) {
+    return "<choose><when test=\""
+        + parameter
+        + "."
+        + c.property()
+        + " != null\">AND "
+        + xml(quote(c.name))
+        + " = "
+        + c.parameter(parameter)
+        + "</when><otherwise>AND "
+        + xml(quote(c.name))
+        + " IS NULL</otherwise></choose>";
+  }
+
+  static String quote(String name) {
+    return "\"" + name.replace("\"", "\"\"") + "\"";
+  }
+
+  static String javaString(String value) {
+    return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
+  }
+
+  static String xml(String value) {
+    return value
+        .replace("&", "&amp;")
+        .replace("<", "&lt;")
+        .replace(">", "&gt;")
+        .replace("\"", "&quot;");
+  }
+
+  static class Column {
+    final String name;
+    final String type;
+    final String category;
+
+    Column(String name, String type, String category) {
+      this.name = name;
+      this.type = type;
+      this.category = category;
+    }
+
+    boolean key() {
+      return category.equals("TIME") || category.equals("TAG");
+    }
+
+    String property() {
+      String[] parts = name.split("_");
+      StringBuilder result = new StringBuilder(parts[0]);
+      for (int i = 1; i < parts.length; i++) {
+        if (!parts[i].isEmpty())
+          result.append(Character.toUpperCase(parts[i].charAt(0))).append(parts[i].substring(1));
+      }
+      return result.toString();
+    }
+
+    String javaType() {
+      switch (type) {
+        case "TIMESTAMP":
+        case "INT64":
+          return "Long";
+        case "INT32":
+          return "Integer";
+        case "FLOAT":
+          return "Float";
+        case "DOUBLE":
+          return "Double";
+        case "BOOLEAN":
+          return "Boolean";
+        case "DATE":
+          return "java.time.LocalDate";
+        case "BLOB":
+          return "byte[]";
+        case "STRING":
+        case "TEXT":
+          return "String";
+        default:
+          throw new IllegalArgumentException("Unsupported IoTDB type: " + type);
+      }
+    }
+
+    String jdbcType() {
+      switch (type) {
+        case "INT64":
+          return "BIGINT";
+        case "INT32":
+          return "INTEGER";
+        case "STRING":
+        case "TEXT":
+          return "VARCHAR";
+        default:
+          return type;
+      }
+    }
+
+    String handler() {
+      if (type.equals("DATE")) return "org.apache.iotdb.mybatis.type.IoTDBLocalDateTypeHandler";
+      if (type.equals("BLOB")) return "org.apache.iotdb.mybatis.type.IoTDBBlobTypeHandler";
+      return null;
+    }
+
+    String parameter(String prefix) {
+      return "#{"
+          + prefix
+          + "."
+          + property()
+          + ",jdbcType="
+          + jdbcType()
+          + (handler() == null ? "" : ",typeHandler=" + handler())
+          + "}";
+    }
+  }
+}
diff --git a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/Main.java b/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/Main.java
index 9db7b82..acd8f35 100644
--- a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/Main.java
+++ b/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/Main.java
@@ -19,96 +19,16 @@
 
 package org.apache.iotdb;
 
-import org.apache.iotdb.jdbc.IoTDBDataSource;
-
-import com.baomidou.mybatisplus.generator.FastAutoGenerator;
-import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
-import com.baomidou.mybatisplus.generator.config.OutputFile;
-import com.baomidou.mybatisplus.generator.config.rules.DateType;
-import com.baomidou.mybatisplus.generator.config.rules.DbColumnType;
 import org.mybatis.spring.annotation.MapperScan;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 
-import java.sql.Types;
-import java.util.Collections;
-
 @SpringBootApplication
-@MapperScan("org.apache.iotdb.mapper")
+@MapperScan(
+    value = "org.apache.iotdb.mapper",
+    annotationClass = org.apache.ibatis.annotations.Mapper.class)
 public class Main {
   public static void main(String[] args) {
     SpringApplication.run(Main.class, args);
-    IoTDBDataSource dataSource = new IoTDBDataSource();
-    dataSource.setUrl("jdbc:iotdb://127.0.0.1:6667/database1?sql_dialect=table");
-    dataSource.setUser("root");
-    dataSource.setPassword("root");
-    FastAutoGenerator generator =
-        FastAutoGenerator.create(
-            new DataSourceConfig.Builder(dataSource)
-                .driverClassName("org.apache.iotdb.jdbc.IoTDBDriver"));
-    generator
-        .globalConfig(
-            builder -> {
-              builder
-                  .author("IoTDB")
-                  .enableSwagger()
-                  .dateType(DateType.ONLY_DATE)
-                  .outputDir("src/main/java");
-            })
-        .packageConfig(
-            builder -> {
-              builder
-                  .parent("org.apache.iotdb")
-                  .mapper("mapper")
-                  .pathInfo(
-                      Collections.singletonMap(
-                          OutputFile.xml, "src/main/java/org/apache/iotdb/xml"));
-            })
-        .dataSourceConfig(
-            builder -> {
-              builder.typeConvertHandler(
-                  (globalConfig, typeRegistry, metaInfo) -> {
-                    int typeCode = metaInfo.getJdbcType().TYPE_CODE;
-                    switch (typeCode) {
-                      case Types.FLOAT:
-                        return DbColumnType.FLOAT;
-                      default:
-                        return typeRegistry.getColumnType(metaInfo);
-                    }
-                  });
-            })
-        .strategyConfig(
-            builder -> {
-              builder.addInclude("table1");
-              builder
-                  .entityBuilder()
-                  .enableLombok()
-                  //                            .addIgnoreColumns("create_time")
-                  .enableFileOverride();
-              builder
-                  .serviceBuilder()
-                  .formatServiceFileName("%sService")
-                  .formatServiceImplFileName("%sServiceImpl")
-                  .convertServiceFileName((entityName -> entityName + "Service"))
-                  .enableFileOverride();
-              builder.controllerBuilder().enableRestStyle().enableFileOverride();
-            })
-        .strategyConfig(
-            builder -> {
-              builder.addInclude("table2");
-              builder
-                  .entityBuilder()
-                  .enableLombok()
-                  //                            .addIgnoreColumns("create_time")
-                  .enableFileOverride();
-              builder
-                  .serviceBuilder()
-                  .formatServiceFileName("%sService")
-                  .formatServiceImplFileName("%sServiceImpl")
-                  .convertServiceFileName((entityName -> entityName + "Service"))
-                  .enableFileOverride();
-              builder.controllerBuilder().enableRestStyle().enableFileOverride();
-            })
-        .execute();
   }
 }
diff --git a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/config/IoTDBMybatisConfiguration.java b/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/config/IoTDBMybatisConfiguration.java
new file mode 100644
index 0000000..82d1295
--- /dev/null
+++ b/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/config/IoTDBMybatisConfiguration.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.iotdb.config;
+
+import org.apache.iotdb.mapper.IoTDBTableMapper;
+import org.apache.iotdb.mybatis.IoTDBQueryInterceptor;
+
+import com.baomidou.mybatisplus.core.injector.AbstractMethod;
+import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
+import com.baomidou.mybatisplus.core.injector.ISqlInjector;
+import com.baomidou.mybatisplus.core.injector.methods.Insert;
+import com.baomidou.mybatisplus.core.injector.methods.SelectList;
+import com.baomidou.mybatisplus.core.metadata.TableInfo;
+import org.apache.ibatis.session.Configuration;
+import org.springframework.context.annotation.Bean;
+
+import java.util.List;
+
+@org.springframework.context.annotation.Configuration(proxyBeanMethods = false)
+public class IoTDBMybatisConfiguration {
+  @Bean
+  public IoTDBQueryInterceptor ioTDBQueryInterceptor() {
+    return new IoTDBQueryInterceptor();
+  }
+
+  @Bean
+  public ISqlInjector ioTDBSqlInjector() {
+    return new DefaultSqlInjector() {
+      @Override
+      public List<AbstractMethod> getMethodList(
+          Configuration configuration, Class<?> mapperClass, TableInfo tableInfo) {
+        return IoTDBTableMapper.class.isAssignableFrom(mapperClass)
+            ? List.of(new Insert(), new SelectList())
+            : super.getMethodList(configuration, mapperClass, tableInfo);
+      }
+    };
+  }
+}
diff --git a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/entity/Table1.java b/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/entity/Table1.java
index 9e324d0..47ad9b3 100644
--- a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/entity/Table1.java
+++ b/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/entity/Table1.java
@@ -1,71 +1,66 @@
 /*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.apache.iotdb.entity;
 
-import com.baomidou.mybatisplus.annotation.TableField;
-import com.github.jeffreyning.mybatisplus.anno.MppMultiId;
-import io.swagger.annotations.ApiModel;
-import lombok.Getter;
-import lombok.Setter;
-import lombok.ToString;
-
-import java.io.Serializable;
-import java.util.Date;
-
-/**
- * @author IoTDB
- * @since 2025-06-24
- */
-@Getter
-@Setter
-@ToString
-@ApiModel(value = "Table1对象", description = "")
-public class Table1 implements Serializable {
+import org.apache.iotdb.mybatis.type.IoTDBBlobTypeHandler;
+import org.apache.iotdb.mybatis.type.IoTDBLocalDateTypeHandler;
 
-  private static final long serialVersionUID = 1L;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
 
-  @MppMultiId
-  @TableField("time")
-  private Date time;
+/** TIME and all TAGs identify a row; timestamp values use the server's configured precision. */
+@Data
+@TableName(value = "table1", autoResultMap = true)
+public class Table1 {
+  @TableField(value = "\"time\"")
+  private Long time;
 
-  @MppMultiId
-  @TableField("region")
+  @TableField(value = "\"region\"")
   private String region;
 
-  @MppMultiId
-  @TableField("plant_id")
+  @TableField(value = "\"plant_id\"")
   private String plantId;
 
-  @MppMultiId
-  @TableField("device_id")
+  @TableField(value = "\"device_id\"")
   private String deviceId;
 
+  @TableField(value = "\"model_id\"")
   private String modelId;
 
+  @TableField(value = "\"maintenance\"")
   private String maintenance;
 
+  @TableField(value = "\"temperature\"")
   private Float temperature;
 
+  @TableField(value = "\"humidity\"")
   private Float humidity;
 
+  @TableField(value = "\"status\"")
   private Boolean status;
 
-  private Date arrivalTime;
+  @TableField(value = "\"arrival_time\"")
+  private Long arrivalTime;
+
+  @TableField(value = "\"reading_date\"", typeHandler = IoTDBLocalDateTypeHandler.class)
+  private java.time.LocalDate readingDate;
+
+  @TableField(value = "\"payload\"", typeHandler = IoTDBBlobTypeHandler.class)
+  private byte[] payload;
 }
diff --git a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/entity/Table2.java b/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/entity/Table2.java
index 2713354..6ec59e3 100644
--- a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/entity/Table2.java
+++ b/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/entity/Table2.java
@@ -1,71 +1,66 @@
 /*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.apache.iotdb.entity;
 
-import com.baomidou.mybatisplus.annotation.TableField;
-import com.github.jeffreyning.mybatisplus.anno.MppMultiId;
-import io.swagger.annotations.ApiModel;
-import lombok.Getter;
-import lombok.Setter;
-import lombok.ToString;
-
-import java.io.Serializable;
-import java.util.Date;
-
-/**
- * @author IoTDB
- * @since 2025-06-24
- */
-@Getter
-@Setter
-@ToString
-@ApiModel(value = "Table2对象", description = "")
-public class Table2 implements Serializable {
+import org.apache.iotdb.mybatis.type.IoTDBBlobTypeHandler;
+import org.apache.iotdb.mybatis.type.IoTDBLocalDateTypeHandler;
 
-  private static final long serialVersionUID = 1L;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
 
-  @MppMultiId
-  @TableField("time")
-  private Date time;
+/** TIME and all TAGs identify a row; timestamp values use the server's configured precision. */
+@Data
+@TableName(value = "table2", autoResultMap = true)
+public class Table2 {
+  @TableField(value = "\"time\"")
+  private Long time;
 
-  @MppMultiId
-  @TableField("region")
+  @TableField(value = "\"region\"")
   private String region;
 
-  @MppMultiId
-  @TableField("plant_id")
+  @TableField(value = "\"plant_id\"")
   private String plantId;
 
-  @MppMultiId
-  @TableField("device_id")
+  @TableField(value = "\"device_id\"")
   private String deviceId;
 
+  @TableField(value = "\"model_id\"")
   private String modelId;
 
+  @TableField(value = "\"maintenance\"")
   private String maintenance;
 
+  @TableField(value = "\"temperature\"")
   private Float temperature;
 
+  @TableField(value = "\"humidity\"")
   private Float humidity;
 
+  @TableField(value = "\"status\"")
   private Boolean status;
 
-  private Date arrivalTime;
+  @TableField(value = "\"arrival_time\"")
+  private Long arrivalTime;
+
+  @TableField(value = "\"reading_date\"", typeHandler = IoTDBLocalDateTypeHandler.class)
+  private java.time.LocalDate readingDate;
+
+  @TableField(value = "\"payload\"", typeHandler = IoTDBBlobTypeHandler.class)
+  private byte[] payload;
 }
diff --git a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/mapper/IoTDBTableMapper.java b/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/mapper/IoTDBTableMapper.java
new file mode 100644
index 0000000..f20f195
--- /dev/null
+++ b/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/mapper/IoTDBTableMapper.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.iotdb.mapper;
+
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.mapper.Mapper;
+import com.baomidou.mybatisplus.core.toolkit.Constants;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/** Deliberately exposes only operations supported by IoTDB's table model. */
+public interface IoTDBTableMapper<T> extends Mapper<T> {
+  int insert(T entity);
+
+  List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> query);
+
+  T selectByKey(@Param("key") T key);
+
+  int deleteByKey(@Param("key") T key);
+
+  int upsertFields(@Param("row") T row);
+
+  int updateAttributes(@Param("row") T row);
+}
diff --git a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/mapper/Table1Mapper.java b/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/mapper/Table1Mapper.java
index 40c0f26..3dcbf5a 100644
--- a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/mapper/Table1Mapper.java
+++ b/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/mapper/Table1Mapper.java
@@ -1,32 +1,25 @@
 /*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.apache.iotdb.mapper;
 
 import org.apache.iotdb.entity.Table1;
 
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
 
-/**
- * Mapper 接口
- *
- * @author IoTDB
- * @since 2025-06-24
- */
-public interface Table1Mapper extends BaseMapper<Table1> {}
+@Mapper
+public interface Table1Mapper extends IoTDBTableMapper<Table1> {}
diff --git a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/mapper/Table2Mapper.java b/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/mapper/Table2Mapper.java
index bf9ba7d..cd2909e 100644
--- a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/mapper/Table2Mapper.java
+++ b/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/mapper/Table2Mapper.java
@@ -1,32 +1,25 @@
 /*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.apache.iotdb.mapper;
 
 import org.apache.iotdb.entity.Table2;
 
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
 
-/**
- * Mapper 接口
- *
- * @author IoTDB
- * @since 2025-06-24
- */
-public interface Table2Mapper extends BaseMapper<Table2> {}
+@Mapper
+public interface Table2Mapper extends IoTDBTableMapper<Table2> {}
diff --git a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/service/Table1Service.java b/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/service/Table1Service.java
index bcd980b..9cb9559 100644
--- a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/service/Table1Service.java
+++ b/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/service/Table1Service.java
@@ -1,32 +1,36 @@
 /*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.apache.iotdb.service;
 
 import org.apache.iotdb.entity.Table1;
 
-import com.baomidou.mybatisplus.extension.service.IService;
+import java.util.List;
 
-/**
- * 服务类
- *
- * @author IoTDB
- * @since 2025-06-24
- */
-public interface Table1Service extends IService<Table1> {}
+public interface Table1Service {
+  int insert(Table1 row);
+
+  Table1 selectByKey(Table1 key);
+
+  int deleteByKey(Table1 key);
+
+  int upsertFields(Table1 row);
+
+  int updateAttributes(Table1 row);
+
+  List<Table1> list();
+}
diff --git a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/service/Table2Service.java b/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/service/Table2Service.java
index bc63069..908e260 100644
--- a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/service/Table2Service.java
+++ b/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/service/Table2Service.java
@@ -1,32 +1,36 @@
 /*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.apache.iotdb.service;
 
 import org.apache.iotdb.entity.Table2;
 
-import com.baomidou.mybatisplus.extension.service.IService;
+import java.util.List;
 
-/**
- * 服务类
- *
- * @author IoTDB
- * @since 2025-06-24
- */
-public interface Table2Service extends IService<Table2> {}
+public interface Table2Service {
+  int insert(Table2 row);
+
+  Table2 selectByKey(Table2 key);
+
+  int deleteByKey(Table2 key);
+
+  int upsertFields(Table2 row);
+
+  int updateAttributes(Table2 row);
+
+  List<Table2> list();
+}
diff --git a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/service/impl/Table1ServiceImpl.java b/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/service/impl/Table1ServiceImpl.java
index 8ccde7e..ddc84f3 100644
--- a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/service/impl/Table1ServiceImpl.java
+++ b/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/service/impl/Table1ServiceImpl.java
@@ -1,20 +1,18 @@
 /*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.apache.iotdb.service.impl;
@@ -23,14 +21,70 @@ import org.apache.iotdb.entity.Table1;
 import org.apache.iotdb.mapper.Table1Mapper;
 import org.apache.iotdb.service.Table1Service;
 
-import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import org.springframework.stereotype.Service;
 
-/**
- * 服务实现类
- *
- * @author IoTDB
- * @since 2025-06-24
- */
+import java.util.List;
+import java.util.Objects;
+
 @Service
-public class Table1ServiceImpl extends ServiceImpl<Table1Mapper, Table1> implements Table1Service {}
+public class Table1ServiceImpl implements Table1Service {
+  private final Table1Mapper mapper;
+
+  public Table1ServiceImpl(Table1Mapper mapper) {
+    this.mapper = mapper;
+  }
+
+  private void requireTime(Table1 row) {
+    Objects.requireNonNull(row, "row");
+    Objects.requireNonNull(row.getTime(), "time is required; supply raw server-precision ticks");
+    // Null TAGs are valid IoTDB key components; the XML generates IS NULL predicates for them.
+  }
+
+  @Override
+  public int insert(Table1 row) {
+    requireTime(row);
+    return mapper.insert(row);
+  }
+
+  @Override
+  public Table1 selectByKey(Table1 key) {
+    requireTime(key);
+    return mapper.selectByKey(key);
+  }
+
+  @Override
+  public int deleteByKey(Table1 key) {
+    requireTime(key);
+    return mapper.deleteByKey(key);
+  }
+
+  @Override
+  public int upsertFields(Table1 row) {
+    requireTime(row);
+    if (row.getTemperature() == null
+        && row.getHumidity() == null
+        && row.getStatus() == null
+        && row.getArrivalTime() == null
+        && row.getReadingDate() == null
+        && row.getPayload() == null) {
+      throw new IllegalArgumentException("at least one non-null FIELD value is required");
+    }
+    return mapper.upsertFields(row);
+  }
+
+  @Override
+  public int updateAttributes(Table1 row) {
+    Objects.requireNonNull(row, "row");
+    // Attributes belong to the device (all TAGs), not to an individual timestamp.
+    return mapper.updateAttributes(row);
+  }
+
+  @Override
+  public List<Table1> list() {
+    return mapper.selectList(
+        new QueryWrapper<Table1>()
+            .orderByAsc("time", "region", "plant_id", "device_id")
+            .last("LIMIT 1000"));
+  }
+}
diff --git a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/service/impl/Table2ServiceImpl.java b/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/service/impl/Table2ServiceImpl.java
index 6de7a68..a4f06c8 100644
--- a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/service/impl/Table2ServiceImpl.java
+++ b/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/service/impl/Table2ServiceImpl.java
@@ -1,20 +1,18 @@
 /*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.apache.iotdb.service.impl;
@@ -23,14 +21,70 @@ import org.apache.iotdb.entity.Table2;
 import org.apache.iotdb.mapper.Table2Mapper;
 import org.apache.iotdb.service.Table2Service;
 
-import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import org.springframework.stereotype.Service;
 
-/**
- * 服务实现类
- *
- * @author IoTDB
- * @since 2025-06-24
- */
+import java.util.List;
+import java.util.Objects;
+
 @Service
-public class Table2ServiceImpl extends ServiceImpl<Table2Mapper, Table2> implements Table2Service {}
+public class Table2ServiceImpl implements Table2Service {
+  private final Table2Mapper mapper;
+
+  public Table2ServiceImpl(Table2Mapper mapper) {
+    this.mapper = mapper;
+  }
+
+  private void requireTime(Table2 row) {
+    Objects.requireNonNull(row, "row");
+    Objects.requireNonNull(row.getTime(), "time is required; supply raw server-precision ticks");
+    // Null TAGs are valid IoTDB key components; the XML generates IS NULL predicates for them.
+  }
+
+  @Override
+  public int insert(Table2 row) {
+    requireTime(row);
+    return mapper.insert(row);
+  }
+
+  @Override
+  public Table2 selectByKey(Table2 key) {
+    requireTime(key);
+    return mapper.selectByKey(key);
+  }
+
+  @Override
+  public int deleteByKey(Table2 key) {
+    requireTime(key);
+    return mapper.deleteByKey(key);
+  }
+
+  @Override
+  public int upsertFields(Table2 row) {
+    requireTime(row);
+    if (row.getTemperature() == null
+        && row.getHumidity() == null
+        && row.getStatus() == null
+        && row.getArrivalTime() == null
+        && row.getReadingDate() == null
+        && row.getPayload() == null) {
+      throw new IllegalArgumentException("at least one non-null FIELD value is required");
+    }
+    return mapper.upsertFields(row);
+  }
+
+  @Override
+  public int updateAttributes(Table2 row) {
+    Objects.requireNonNull(row, "row");
+    // Attributes belong to the device (all TAGs), not to an individual timestamp.
+    return mapper.updateAttributes(row);
+  }
+
+  @Override
+  public List<Table2> list() {
+    return mapper.selectList(
+        new QueryWrapper<Table2>()
+            .orderByAsc("time", "region", "plant_id", "device_id")
+            .last("LIMIT 1000"));
+  }
+}
diff --git a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/xml/Table1Mapper.xml b/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/xml/Table1Mapper.xml
deleted file mode 100644
index 77532cc..0000000
--- a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/xml/Table1Mapper.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-
-<!--
-
-    Licensed to the Apache Software Foundation (ASF) under one
-    or more contributor license agreements.  See the NOTICE file
-    distributed with this work for additional information
-    regarding copyright ownership.  The ASF licenses this file
-    to you under the Apache License, Version 2.0 (the
-    "License"); you may not use this file except in compliance
-    with the License.  You may obtain a copy of the License at
-
-        http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing,
-    software distributed under the License is distributed on an
-    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-    KIND, either express or implied.  See the License for the
-    specific language governing permissions and limitations
-    under the License.
-
--->
-<mapper namespace="org.apache.iotdb.mapper.Table1Mapper"/>
diff --git a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/xml/Table2Mapper.xml b/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/xml/Table2Mapper.xml
deleted file mode 100644
index ea6a959..0000000
--- a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/xml/Table2Mapper.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-
-<!--
-
-    Licensed to the Apache Software Foundation (ASF) under one
-    or more contributor license agreements.  See the NOTICE file
-    distributed with this work for additional information
-    regarding copyright ownership.  The ASF licenses this file
-    to you under the Apache License, Version 2.0 (the
-    "License"); you may not use this file except in compliance
-    with the License.  You may obtain a copy of the License at
-
-        http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing,
-    software distributed under the License is distributed on an
-    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-    KIND, either express or implied.  See the License for the
-    specific language governing permissions and limitations
-    under the License.
-
--->
-<mapper namespace="org.apache.iotdb.mapper.Table2Mapper"/>
diff --git a/examples/mybatisplus-generator/src/main/resources/application.yml b/examples/mybatisplus-generator/src/main/resources/application.yml
index bdf355a..5a7c0f3 100644
--- a/examples/mybatisplus-generator/src/main/resources/application.yml
+++ b/examples/mybatisplus-generator/src/main/resources/application.yml
@@ -21,3 +21,6 @@ spring:
     username: root
     password: root
     driver-class-name: org.apache.iotdb.jdbc.IoTDBDriver
+mybatis-plus:
+  mapper-locations: classpath*:mappers/*.xml
+  type-handlers-package: org.apache.iotdb.mybatis.type
diff --git a/examples/mybatisplus-generator/src/main/resources/mappers/Table1Mapper.xml b/examples/mybatisplus-generator/src/main/resources/mappers/Table1Mapper.xml
new file mode 100644
index 0000000..db275b7
--- /dev/null
+++ b/examples/mybatisplus-generator/src/main/resources/mappers/Table1Mapper.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<mapper namespace="org.apache.iotdb.mapper.Table1Mapper">
+    <resultMap id="Row" type="org.apache.iotdb.entity.Table1">
+        <id column="time" property="time" jdbcType="TIMESTAMP"/>
+        <id column="region" property="region" jdbcType="VARCHAR"/>
+        <id column="plant_id" property="plantId" jdbcType="VARCHAR"/>
+        <id column="device_id" property="deviceId" jdbcType="VARCHAR"/>
+        <result column="model_id" property="modelId" jdbcType="VARCHAR"/>
+        <result column="maintenance" property="maintenance" jdbcType="VARCHAR"/>
+        <result column="temperature" property="temperature" jdbcType="FLOAT"/>
+        <result column="humidity" property="humidity" jdbcType="FLOAT"/>
+        <result column="status" property="status" jdbcType="BOOLEAN"/>
+        <result column="arrival_time" property="arrivalTime" jdbcType="TIMESTAMP"/>
+        <result column="reading_date" property="readingDate" jdbcType="DATE" typeHandler="org.apache.iotdb.mybatis.type.IoTDBLocalDateTypeHandler"/>
+        <result column="payload" property="payload" jdbcType="BLOB" typeHandler="org.apache.iotdb.mybatis.type.IoTDBBlobTypeHandler"/>
+    </resultMap>
+    <sql id="RowKey">"time" = #{key.time,jdbcType=TIMESTAMP}
+        <choose>
+            <when test="key.region != null">AND "region" = #{key.region,jdbcType=VARCHAR}</when>
+            <otherwise>AND "region" IS NULL</otherwise>
+        </choose>
+        <choose>
+            <when test="key.plantId != null">AND "plant_id" = #{key.plantId,jdbcType=VARCHAR}</when>
+            <otherwise>AND "plant_id" IS NULL</otherwise>
+        </choose>
+        <choose>
+            <when test="key.deviceId != null">AND "device_id" = #{key.deviceId,jdbcType=VARCHAR}</when>
+            <otherwise>AND "device_id" IS NULL</otherwise>
+        </choose>
+    </sql>
+    <select id="selectByKey" resultMap="Row">SELECT "time", "region", "plant_id", "device_id", "model_id", "maintenance", "temperature", "humidity", "status", "arrival_time", "reading_date", "payload" FROM "table1" WHERE
+        <include refid="RowKey"/>
+    </select>
+    <delete id="deleteByKey">DELETE FROM "table1" WHERE
+        <include refid="RowKey"/>
+    </delete>
+    <insert id="upsertFields">INSERT INTO "table1"
+        <trim prefix="(" suffix=")" suffixOverrides=",">"time", "region", "plant_id", "device_id",
+            <if test="row.temperature != null">"temperature",</if>
+            <if test="row.humidity != null">"humidity",</if>
+            <if test="row.status != null">"status",</if>
+            <if test="row.arrivalTime != null">"arrival_time",</if>
+            <if test="row.readingDate != null">"reading_date",</if>
+            <if test="row.payload != null">"payload",</if>
+        </trim>VALUES
+        <trim prefix="(" suffix=")" suffixOverrides=",">#{row.time,jdbcType=TIMESTAMP}, #{row.region,jdbcType=VARCHAR}, #{row.plantId,jdbcType=VARCHAR}, #{row.deviceId,jdbcType=VARCHAR},
+            <if test="row.temperature != null">#{row.temperature,jdbcType=FLOAT},</if>
+            <if test="row.humidity != null">#{row.humidity,jdbcType=FLOAT},</if>
+            <if test="row.status != null">#{row.status,jdbcType=BOOLEAN},</if>
+            <if test="row.arrivalTime != null">#{row.arrivalTime,jdbcType=TIMESTAMP},</if>
+            <if test="row.readingDate != null">#{row.readingDate,jdbcType=DATE,typeHandler=org.apache.iotdb.mybatis.type.IoTDBLocalDateTypeHandler},</if>
+            <if test="row.payload != null">#{row.payload,jdbcType=BLOB,typeHandler=org.apache.iotdb.mybatis.type.IoTDBBlobTypeHandler},</if>
+        </trim>
+    </insert>
+    <update id="updateAttributes">UPDATE "table1" SET "model_id" = #{row.modelId,jdbcType=VARCHAR}, "maintenance" = #{row.maintenance,jdbcType=VARCHAR}
+        <where>
+            <choose>
+                <when test="row.region != null">AND "region" = #{row.region,jdbcType=VARCHAR}</when>
+                <otherwise>AND "region" IS NULL</otherwise>
+            </choose>
+            <choose>
+                <when test="row.plantId != null">AND "plant_id" = #{row.plantId,jdbcType=VARCHAR}</when>
+                <otherwise>AND "plant_id" IS NULL</otherwise>
+            </choose>
+            <choose>
+                <when test="row.deviceId != null">AND "device_id" = #{row.deviceId,jdbcType=VARCHAR}</when>
+                <otherwise>AND "device_id" IS NULL</otherwise>
+            </choose>
+        </where>
+    </update>
+</mapper>
diff --git a/examples/mybatisplus-generator/src/main/resources/mappers/Table2Mapper.xml b/examples/mybatisplus-generator/src/main/resources/mappers/Table2Mapper.xml
new file mode 100644
index 0000000..4c91138
--- /dev/null
+++ b/examples/mybatisplus-generator/src/main/resources/mappers/Table2Mapper.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<mapper namespace="org.apache.iotdb.mapper.Table2Mapper">
+    <resultMap id="Row" type="org.apache.iotdb.entity.Table2">
+        <id column="time" property="time" jdbcType="TIMESTAMP"/>
+        <id column="region" property="region" jdbcType="VARCHAR"/>
+        <id column="plant_id" property="plantId" jdbcType="VARCHAR"/>
+        <id column="device_id" property="deviceId" jdbcType="VARCHAR"/>
+        <result column="model_id" property="modelId" jdbcType="VARCHAR"/>
+        <result column="maintenance" property="maintenance" jdbcType="VARCHAR"/>
+        <result column="temperature" property="temperature" jdbcType="FLOAT"/>
+        <result column="humidity" property="humidity" jdbcType="FLOAT"/>
+        <result column="status" property="status" jdbcType="BOOLEAN"/>
+        <result column="arrival_time" property="arrivalTime" jdbcType="TIMESTAMP"/>
+        <result column="reading_date" property="readingDate" jdbcType="DATE" typeHandler="org.apache.iotdb.mybatis.type.IoTDBLocalDateTypeHandler"/>
+        <result column="payload" property="payload" jdbcType="BLOB" typeHandler="org.apache.iotdb.mybatis.type.IoTDBBlobTypeHandler"/>
+    </resultMap>
+    <sql id="RowKey">"time" = #{key.time,jdbcType=TIMESTAMP}
+        <choose>
+            <when test="key.region != null">AND "region" = #{key.region,jdbcType=VARCHAR}</when>
+            <otherwise>AND "region" IS NULL</otherwise>
+        </choose>
+        <choose>
+            <when test="key.plantId != null">AND "plant_id" = #{key.plantId,jdbcType=VARCHAR}</when>
+            <otherwise>AND "plant_id" IS NULL</otherwise>
+        </choose>
+        <choose>
+            <when test="key.deviceId != null">AND "device_id" = #{key.deviceId,jdbcType=VARCHAR}</when>
+            <otherwise>AND "device_id" IS NULL</otherwise>
+        </choose>
+    </sql>
+    <select id="selectByKey" resultMap="Row">SELECT "time", "region", "plant_id", "device_id", "model_id", "maintenance", "temperature", "humidity", "status", "arrival_time", "reading_date", "payload" FROM "table2" WHERE
+        <include refid="RowKey"/>
+    </select>
+    <delete id="deleteByKey">DELETE FROM "table2" WHERE
+        <include refid="RowKey"/>
+    </delete>
+    <insert id="upsertFields">INSERT INTO "table2"
+        <trim prefix="(" suffix=")" suffixOverrides=",">"time", "region", "plant_id", "device_id",
+            <if test="row.temperature != null">"temperature",</if>
+            <if test="row.humidity != null">"humidity",</if>
+            <if test="row.status != null">"status",</if>
+            <if test="row.arrivalTime != null">"arrival_time",</if>
+            <if test="row.readingDate != null">"reading_date",</if>
+            <if test="row.payload != null">"payload",</if>
+        </trim>VALUES
+        <trim prefix="(" suffix=")" suffixOverrides=",">#{row.time,jdbcType=TIMESTAMP}, #{row.region,jdbcType=VARCHAR}, #{row.plantId,jdbcType=VARCHAR}, #{row.deviceId,jdbcType=VARCHAR},
+            <if test="row.temperature != null">#{row.temperature,jdbcType=FLOAT},</if>
+            <if test="row.humidity != null">#{row.humidity,jdbcType=FLOAT},</if>
+            <if test="row.status != null">#{row.status,jdbcType=BOOLEAN},</if>
+            <if test="row.arrivalTime != null">#{row.arrivalTime,jdbcType=TIMESTAMP},</if>
+            <if test="row.readingDate != null">#{row.readingDate,jdbcType=DATE,typeHandler=org.apache.iotdb.mybatis.type.IoTDBLocalDateTypeHandler},</if>
+            <if test="row.payload != null">#{row.payload,jdbcType=BLOB,typeHandler=org.apache.iotdb.mybatis.type.IoTDBBlobTypeHandler},</if>
+        </trim>
+    </insert>
+    <update id="updateAttributes">UPDATE "table2" SET "model_id" = #{row.modelId,jdbcType=VARCHAR}, "maintenance" = #{row.maintenance,jdbcType=VARCHAR}
+        <where>
+            <choose>
+                <when test="row.region != null">AND "region" = #{row.region,jdbcType=VARCHAR}</when>
+                <otherwise>AND "region" IS NULL</otherwise>
+            </choose>
+            <choose>
+                <when test="row.plantId != null">AND "plant_id" = #{row.plantId,jdbcType=VARCHAR}</when>
+                <otherwise>AND "plant_id" IS NULL</otherwise>
+            </choose>
+            <choose>
+                <when test="row.deviceId != null">AND "device_id" = #{row.deviceId,jdbcType=VARCHAR}</when>
+                <otherwise>AND "device_id" IS NULL</otherwise>
+            </choose>
+        </where>
+    </update>
+</mapper>
diff --git a/examples/mybatisplus-generator/src/main/resources/templates/iotdb-entity.java.vm b/examples/mybatisplus-generator/src/main/resources/templates/iotdb-entity.java.vm
new file mode 100644
index 0000000..d5b75fb
--- /dev/null
+++ b/examples/mybatisplus-generator/src/main/resources/templates/iotdb-entity.java.vm
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ${package.Entity};
+
+@lombok.Data
+@com.baomidou.mybatisplus.annotation.TableName(value = ${iotdbJavaTable}, autoResultMap = true)
+public class ${entity} {
+${iotdbFields}
+}
diff --git a/examples/mybatisplus-generator/src/main/resources/templates/iotdb-mapper.java.vm b/examples/mybatisplus-generator/src/main/resources/templates/iotdb-mapper.java.vm
new file mode 100644
index 0000000..d5dcf24
--- /dev/null
+++ b/examples/mybatisplus-generator/src/main/resources/templates/iotdb-mapper.java.vm
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ${package.Mapper};
+
+import ${package.Entity}.${entity};
+import org.apache.iotdb.mapper.IoTDBTableMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ${table.mapperName} extends IoTDBTableMapper<${entity}> {
+#if(!$iotdbHasAttributes)
+  @Override
+  default int updateAttributes(${entity} row) {
+    throw new UnsupportedOperationException("This table has no ATTRIBUTE columns");
+  }
+#end
+}
diff --git a/examples/mybatisplus-generator/src/main/resources/templates/iotdb-mapper.xml.vm b/examples/mybatisplus-generator/src/main/resources/templates/iotdb-mapper.xml.vm
new file mode 100644
index 0000000..328c97f
--- /dev/null
+++ b/examples/mybatisplus-generator/src/main/resources/templates/iotdb-mapper.xml.vm
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="${package.Mapper}.${table.mapperName}">
+  <resultMap id="Row" type="${package.Entity}.${entity}">
+${iotdbResults}
+  </resultMap>
+  <select id="selectByKey" resultMap="Row">
+    SELECT ${iotdbColumns} FROM ${iotdbTable} <where>${iotdbKeyPredicate}</where>
+  </select>
+  <delete id="deleteByKey">
+    DELETE FROM ${iotdbTable} <where>${iotdbKeyPredicate}</where>
+  </delete>
+  <insert id="upsertFields">
+    INSERT INTO ${iotdbTable}
+    <trim prefix="(" suffix=")" suffixOverrides=",">${iotdbUpsertColumns}</trim>
+    VALUES
+    <trim prefix="(" suffix=")" suffixOverrides=",">${iotdbUpsertValues}</trim>
+  </insert>
+#if($iotdbHasAttributes)
+  <update id="updateAttributes">
+    UPDATE ${iotdbTable} SET ${iotdbAttributes}
+    <where>${iotdbTagPredicate}</where>
+  </update>
+#end
+</mapper>
diff --git a/examples/mybatisplus-generator/src/test/java/org/apache/iotdb/ApplicationTest.java b/examples/mybatisplus-generator/src/test/java/org/apache/iotdb/ApplicationTest.java
index 9b9b121..fb9da66 100644
--- a/examples/mybatisplus-generator/src/test/java/org/apache/iotdb/ApplicationTest.java
+++ b/examples/mybatisplus-generator/src/test/java/org/apache/iotdb/ApplicationTest.java
@@ -1,40 +1,156 @@
 /*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.apache.iotdb;
 
+import org.apache.iotdb.entity.Table1;
+import org.apache.iotdb.mapper.Table1Mapper;
+import org.apache.iotdb.mapper.Table2Mapper;
+import org.apache.iotdb.mybatis.type.IoTDBBlobTypeHandler;
+import org.apache.iotdb.mybatis.type.IoTDBLocalDateTypeHandler;
 import org.apache.iotdb.service.Table1Service;
-import org.apache.iotdb.service.Table2Service;
+import org.apache.iotdb.service.impl.Table1ServiceImpl;
 
+import org.apache.ibatis.mapping.BoundSql;
+import org.apache.ibatis.session.SqlSessionFactory;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 
-@SpringBootTest
+import java.lang.reflect.Proxy;
+import java.time.LocalDate;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@SpringBootTest(
+    webEnvironment = SpringBootTest.WebEnvironment.NONE,
+    properties = {
+      "spring.datasource.hikari.initialization-fail-timeout=-1",
+      "spring.datasource.hikari.minimum-idle=0"
+    })
 public class ApplicationTest {
-  @Autowired private Table1Service table1Service;
-  @Autowired private Table2Service table2Service;
-
-//  @Test
-  void contextLoads() {
-    // 启动Spring容器,验证主流程无异常
-    System.out.println("Table1 查询结果:" + table1Service.list());
-    System.out.println("Table2 查询结果:" + table2Service.list());
+  @Autowired private SqlSessionFactory factory;
+  @Autowired private Table1Service service;
+
+  @Test
+  void registersSafeStatementsAndPackagedXmlForBothTables() {
+    for (Class<?> mapper : List.of(Table1Mapper.class, Table2Mapper.class)) {
+      for (String method :
+          List.of(
+              "insert",
+              "selectList",
+              "selectByKey",
+              "deleteByKey",
+              "upsertFields",
+              "updateAttributes")) {
+        assertThat(factory.getConfiguration().hasStatement(mapper.getName() + "." + method))
+            .isTrue();
+      }
+      for (String method : List.of("selectById", "deleteById", "updateById", "update")) {
+        assertThat(factory.getConfiguration().hasStatement(mapper.getName() + "." + method))
+            .isFalse();
+        assertThat(Arrays.stream(mapper.getMethods()).noneMatch(m -> m.getName().equals(method)))
+            .isTrue();
+      }
+    }
+  }
+
+  private BoundSql sql(String method, Object parameters) {
+    return factory
+        .getConfiguration()
+        .getMappedStatement(Table1Mapper.class.getName() + "." + method)
+        .getBoundSql(parameters);
+  }
+
+  private Table1 row() {
+    Table1 row = new Table1();
+    row.setTime(1700000000123456789L);
+    row.setRegion("r");
+    row.setPlantId("p");
+    row.setDeviceId("d");
+    return row;
+  }
+
+  @Test
+  void keysUseTimeAndEveryTagIncludingNull() {
+    Table1 key = row();
+    for (String method : List.of("selectByKey", "deleteByKey")) {
+      BoundSql query = sql(method, Map.of("key", key));
+      assertThat(query.getSql())
+          .contains("\"time\" = ?", "\"region\" = ?", "\"plant_id\" = ?", "\"device_id\" = ?");
+      assertThat(query.getParameterMappings()).hasSize(4);
+      key.setRegion(null);
+      assertThat(sql(method, Map.of("key", key)).getSql()).contains("\"region\" IS NULL");
+      key.setRegion("r");
+    }
+  }
+
+  @Test
+  void fieldsUseInsertWhileAttributesUseDevicePredicates() {
+    Table1 row = row();
+    row.setTemperature(42.5f);
+    row.setReadingDate(LocalDate.of(2024, 2, 29));
+    row.setPayload(new byte[] {0, (byte) 255});
+    BoundSql insert = sql("upsertFields", Map.of("row", row));
+    assertThat(insert.getSql())
+        .contains("INSERT INTO", "\"temperature\"", "\"reading_date\"", "\"payload\"")
+        .doesNotContain("UPDATE", "\"maintenance\"", "\"model_id\"", "\"humidity\"");
+    assertThat(
+            insert.getParameterMappings().stream()
+                .map(m -> m.getTypeHandler().getClass().getName()))
+        .contains(IoTDBBlobTypeHandler.class.getName(), IoTDBLocalDateTypeHandler.class.getName());
+    BoundSql update = sql("updateAttributes", Map.of("row", row));
+    assertThat(update.getSql())
+        .contains("UPDATE", "\"model_id\" = ?", "\"maintenance\" = ?")
+        .doesNotContain("\"time\"", "\"temperature\"");
+    assertThat(update.getParameterMappings()).hasSize(5);
+  }
+
+  @Test
+  void rejectsMissingTimeAndEmptyFieldPatchBeforeAccessingDatabase() {
+    assertThatThrownBy(() -> service.selectByKey(new Table1()))
+        .isInstanceOf(NullPointerException.class);
+    assertThatThrownBy(() -> service.deleteByKey(new Table1()))
+        .isInstanceOf(NullPointerException.class);
+    assertThatThrownBy(() -> service.upsertFields(row()))
+        .isInstanceOf(IllegalArgumentException.class);
+  }
+
+  @Test
+  void fieldWriteCallsTheInsertStatement() {
+    AtomicInteger calls = new AtomicInteger();
+    Table1Mapper mapper =
+        (Table1Mapper)
+            Proxy.newProxyInstance(
+                Table1Mapper.class.getClassLoader(),
+                new Class<?>[] {Table1Mapper.class},
+                (proxy, method, args) -> {
+                  assertThat(method.getName()).isEqualTo("upsertFields");
+                  calls.incrementAndGet();
+                  return 1;
+                });
+    Table1 row = row();
+    row.setTemperature(3.5f);
+    assertThat(new Table1ServiceImpl(mapper).upsertFields(row)).isEqualTo(1);
+    assertThat(calls).hasValue(1);
   }
 }
diff --git a/examples/mybatisplus-generator/src/test/java/org/apache/iotdb/CodeGeneratorTest.java b/examples/mybatisplus-generator/src/test/java/org/apache/iotdb/CodeGeneratorTest.java
new file mode 100644
index 0000000..941c30b
--- /dev/null
+++ b/examples/mybatisplus-generator/src/test/java/org/apache/iotdb/CodeGeneratorTest.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.iotdb;
+
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.VelocityEngine;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import javax.tools.ToolProvider;
+
+import java.io.StringWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class CodeGeneratorTest {
+  @TempDir Path output;
+  static final List<CodeGenerator.Column> COLUMNS =
+      List.of(
+          new CodeGenerator.Column("time", "TIMESTAMP", "TIME"),
+          new CodeGenerator.Column("region", "STRING", "TAG"),
+          new CodeGenerator.Column("device_id", "STRING", "TAG"),
+          new CodeGenerator.Column("description", "STRING", "ATTRIBUTE"),
+          new CodeGenerator.Column("temperature", "FLOAT", "FIELD"),
+          new CodeGenerator.Column("reading_date", "DATE", "FIELD"),
+          new CodeGenerator.Column("payload", "BLOB", "FIELD"));
+
+  static String render(String template, Map<String, Object> data) {
+    Properties properties = new Properties();
+    properties.setProperty("resource.loaders", "class");
+    properties.setProperty(
+        "resource.loader.class.class",
+        "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
+    VelocityEngine engine = new VelocityEngine(properties);
+    engine.init();
+    StringWriter output = new StringWriter();
+    engine.getTemplate("templates/" + template, "UTF-8").merge(new VelocityContext(data), output);
+    return output.toString();
+  }
+
+  @Test
+  void rendersAndCompilesCompositeKeyTemplatesWithDateAndBlobMappings() throws Exception {
+    Map<String, Object> data = new LinkedHashMap<>(CodeGenerator.templateData(COLUMNS));
+    data.put("entity", "GeneratedRow");
+    data.put("table", Map.of("mapperName", "GeneratedRowMapper"));
+    data.put("package", Map.of("Entity", "generated", "Mapper", "generated"));
+    data.put("iotdbTable", "&quot;readings&quot;");
+    data.put("iotdbJavaTable", "\"\\\"readings\\\"\"");
+    String entity = render("iotdb-entity.java.vm", data);
+    String mapper = render("iotdb-mapper.java.vm", data);
+    String xml = render("iotdb-mapper.xml.vm", data);
+    assertThat(entity)
+        .contains("Long time", "java.time.LocalDate readingDate", "byte[] payload")
+        .doesNotContain("@TableId", "@MppMultiId");
+    assertThat(mapper).contains("IoTDBTableMapper<GeneratedRow>");
+    assertThat(xml)
+        .contains(
+            "key.time",
+            "key.region",
+            "key.deviceId",
+            "IS NULL",
+            "IoTDBBlobTypeHandler",
+            "IoTDBLocalDateTypeHandler",
+            "INSERT INTO",
+            "updateAttributes");
+    Files.writeString(output.resolve("GeneratedRow.java"), entity);
+    Files.writeString(output.resolve("GeneratedRowMapper.java"), mapper);
+    assertThat(
+            ToolProvider.getSystemJavaCompiler()
+                .run(
+                    null,
+                    null,
+                    null,
+                    "-classpath",
+                    System.getProperty("java.class.path"),
+                    "-d",
+                    output.toString(),
+                    output.resolve("GeneratedRow.java").toString(),
+                    output.resolve("GeneratedRowMapper.java").toString()))
+        .isZero();
+  }
+
+  @Test
+  void quotesReservedIdentifiersAndRejectsMissingTimeMetadata() {
+    Map<String, Object> data =
+        CodeGenerator.templateData(
+            List.of(
+                new CodeGenerator.Column("time", "TIMESTAMP", "TIME"),
+                new CodeGenerator.Column("order", "STRING", "TAG")));
+    assertThat(data.get("iotdbColumns")).isEqualTo("&quot;time&quot;, &quot;order&quot;");
+    org.assertj.core.api.Assertions.assertThatThrownBy(() -> CodeGenerator.templateData(List.of()))
+        .isInstanceOf(IllegalArgumentException.class);
+    assertThat(data.get("iotdbHasAttributes")).isEqualTo(false);
+    org.assertj.core.api.Assertions.assertThatThrownBy(
+            () ->
+                CodeGenerator.templateData(
+                    List.of(
+                        new CodeGenerator.Column("time", "TIMESTAMP", "TIME"),
+                        new CodeGenerator.Column("class", "STRING", "TAG"))))
+        .isInstanceOf(IllegalArgumentException.class);
+  }
+}
diff --git a/examples/mybatisplus-generator/src/test/java/org/apache/iotdb/IoTDBMapperIT.java b/examples/mybatisplus-generator/src/test/java/org/apache/iotdb/IoTDBMapperIT.java
new file mode 100644
index 0000000..84a0a1d
--- /dev/null
+++ b/examples/mybatisplus-generator/src/test/java/org/apache/iotdb/IoTDBMapperIT.java
@@ -0,0 +1,273 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.iotdb;
+
+import org.apache.iotdb.config.IoTDBMybatisConfiguration;
+import org.apache.iotdb.entity.Table1;
+import org.apache.iotdb.jdbc.IoTDBDataSource;
+import org.apache.iotdb.mybatis.IoTDBQueryInterceptor;
+import org.apache.iotdb.service.Table1Service;
+
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
+import com.baomidou.mybatisplus.core.MybatisSqlSessionFactoryBuilder;
+import com.baomidou.mybatisplus.core.config.GlobalConfig;
+import com.baomidou.mybatisplus.core.toolkit.GlobalConfigUtils;
+import org.apache.ibatis.builder.xml.XMLMapperBuilder;
+import org.apache.ibatis.io.Resources;
+import org.apache.ibatis.mapping.Environment;
+import org.apache.ibatis.session.SqlSession;
+import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.io.TempDir;
+import org.springframework.boot.WebApplicationType;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.context.ConfigurableApplicationContext;
+
+import javax.tools.ToolProvider;
+
+import java.io.InputStream;
+import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.Statement;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Opt in with -Piotdb-mybatis-it; a missing server is a failure, never a skipped test. */
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class IoTDBMapperIT {
+  @TempDir Path output;
+  private final String database = "mp_it_" + UUID.randomUUID().toString().replace("-", "");
+  private final String url =
+      System.getProperty("iotdb.it.url", "jdbc:iotdb://127.0.0.1:6667/?sql_dialect=table");
+  private final String username = System.getProperty("iotdb.it.username", "root");
+  private final String password = System.getProperty("iotdb.it.password", "root");
+  private Connection admin;
+  private ConfigurableApplicationContext context;
+  private Table1Service service;
+  private boolean databaseCreated;
+
+  private String databaseUrl() {
+    int start = url.indexOf('/', "jdbc:iotdb://".length());
+    int query = url.indexOf('?', start);
+    if (start < 0 || query < 0 || !url.contains("sql_dialect=table")) {
+      throw new IllegalArgumentException("iotdb.it.url must include /?sql_dialect=table");
+    }
+    return url.substring(0, start + 1) + database + url.substring(query);
+  }
+
+  private long timestamp() {
+    switch (System.getProperty("iotdb.it.precision", "ms")) {
+      case "ms":
+        return 1700000000123L;
+      case "us":
+        return 1700000000123456L;
+      case "ns":
+        return 1700000000123456789L;
+      default:
+        throw new IllegalArgumentException("iotdb.it.precision must be ms, us or ns");
+    }
+  }
+
+  @BeforeAll
+  void startApplicationAgainstIsolatedDatabase() throws Exception {
+    Class.forName("org.apache.iotdb.jdbc.IoTDBDriver");
+    admin = DriverManager.getConnection(url, username, password);
+    try (Statement sql = admin.createStatement()) {
+      sql.execute("CREATE DATABASE " + database);
+      databaseCreated = true;
+      sql.execute("USE " + database);
+      for (String table : List.of("table1", "table2")) {
+        sql.execute(
+            "CREATE TABLE "
+                + table
+                + " (region STRING TAG, plant_id STRING TAG, "
+                + "device_id STRING TAG, model_id STRING ATTRIBUTE, maintenance STRING ATTRIBUTE, "
+                + "temperature FLOAT FIELD, humidity FLOAT FIELD, status BOOLEAN FIELD, "
+                + "arrival_time TIMESTAMP FIELD, reading_date DATE FIELD, payload BLOB FIELD)");
+      }
+      sql.execute(
+          "CREATE TABLE readings (region STRING TAG, \"order\" STRING TAG, "
+              + "description STRING ATTRIBUTE, temperature FLOAT FIELD, reading_date DATE FIELD, "
+              + "payload BLOB FIELD)");
+    }
+    context =
+        new SpringApplicationBuilder(Main.class)
+            .web(WebApplicationType.NONE)
+            .run(
+                "--spring.datasource.url=" + databaseUrl(),
+                "--spring.datasource.username=" + username,
+                "--spring.datasource.password=" + password,
+                "--spring.datasource.hikari.minimum-idle=0");
+    service = context.getBean(Table1Service.class);
+  }
+
+  @AfterAll
+  void cleanup() throws Exception {
+    if (context != null) context.close();
+    if (admin != null) {
+      try (Connection connection = admin;
+          Statement sql = connection.createStatement()) {
+        if (databaseCreated) sql.execute("DROP DATABASE " + database);
+      }
+    }
+  }
+
+  private Table1 row(long time, String region) {
+    Table1 row = new Table1();
+    row.setTime(time);
+    row.setRegion(region);
+    row.setPlantId("p");
+    row.setDeviceId("d");
+    row.setTemperature(21.5f);
+    row.setHumidity(60.0f);
+    row.setArrivalTime(time + 7);
+    row.setReadingDate(LocalDate.of(2024, 2, 29));
+    row.setPayload(new byte[] {0, 1, 39, 92, (byte) 128, (byte) 255});
+    row.setModelId("initial");
+    row.setMaintenance("yes");
+    return row;
+  }
+
+  @Test
+  void compositeKeysFieldPatchesAttributesAndTypesRoundTrip() {
+    Table1 first = row(timestamp(), "r1");
+    Table1 otherDevice = row(timestamp(), "r2");
+    Table1 later = row(timestamp() + 1, "r1");
+    Table1 nullTag = row(timestamp(), null);
+    nullTag.setReadingDate(null);
+    nullTag.setPayload(null);
+    for (Table1 row : List.of(first, otherDevice, later, nullTag)) service.insert(row);
+    Table1 actual = service.selectByKey(first);
+    assertThat(actual.getTime()).isEqualTo(first.getTime());
+    assertThat(actual.getArrivalTime()).isEqualTo(first.getArrivalTime());
+    assertThat(actual.getReadingDate()).isEqualTo(first.getReadingDate());
+    assertThat(actual.getPayload()).containsExactly(first.getPayload());
+
+    Table1 patch = new Table1();
+    patch.setTime(first.getTime());
+    patch.setRegion(first.getRegion());
+    patch.setPlantId(first.getPlantId());
+    patch.setDeviceId(first.getDeviceId());
+    patch.setTemperature(99.5f);
+    service.upsertFields(patch);
+    actual = service.selectByKey(first);
+    assertThat(actual.getTemperature()).isEqualTo(99.5f);
+    assertThat(actual.getHumidity()).isEqualTo(60.0f);
+    assertThat(actual.getPayload()).containsExactly(first.getPayload());
+    assertThat(service.selectByKey(otherDevice).getTemperature()).isEqualTo(21.5f);
+
+    patch.setModelId("updated");
+    patch.setMaintenance(null);
+    service.updateAttributes(patch);
+    for (Table1 key : List.of(first, later)) {
+      assertThat(service.selectByKey(key).getModelId()).isEqualTo("updated");
+      assertThat(service.selectByKey(key).getMaintenance()).isNull();
+    }
+    assertThat(service.selectByKey(otherDevice).getModelId()).isEqualTo("initial");
+    assertThat(service.selectByKey(nullTag).getReadingDate()).isNull();
+    assertThat(service.selectByKey(nullTag).getPayload()).isNull();
+    assertThat(service.list()).hasSize(4); // MP's injected selectList also uses the handlers.
+    service.deleteByKey(nullTag);
+    assertThat(service.selectByKey(nullTag)).isNull();
+    assertThat(service.selectByKey(first)).isNotNull();
+  }
+
+  @Test
+  void actualMetadataGeneratesCompilableExecutableMappers() throws Exception {
+    IoTDBDataSource source = new IoTDBDataSource();
+    source.setUrl(databaseUrl());
+    source.setUser(username);
+    source.setPassword(password);
+    CodeGenerator.generate(source, database, List.of("readings"), output);
+    Path classes = Files.createDirectories(output.resolve("classes"));
+    List<String> args =
+        new ArrayList<>(
+            List.of("-classpath", System.getProperty("java.class.path"), "-d", classes.toString()));
+    try (java.util.stream.Stream<Path> paths = Files.walk(output.resolve("java"))) {
+      args.addAll(
+          paths
+              .filter(p -> p.toString().endsWith(".java"))
+              .map(Path::toString)
+              .collect(Collectors.toList()));
+    }
+    assertThat(args).hasSizeGreaterThan(4);
+    assertThat(
+            ToolProvider.getSystemJavaCompiler().run(null, null, null, args.toArray(new String[0])))
+        .isZero();
+
+    ClassLoader previous = Resources.getDefaultClassLoader();
+    try (URLClassLoader loader =
+        new URLClassLoader(
+            new java.net.URL[] {classes.toUri().toURL()}, getClass().getClassLoader())) {
+      Resources.setDefaultClassLoader(loader);
+      Class<?> entity = loader.loadClass("org.apache.iotdb.entity.Readings");
+      Class<?> mapper = loader.loadClass("org.apache.iotdb.mapper.ReadingsMapper");
+      MybatisConfiguration config = new MybatisConfiguration();
+      config.setEnvironment(new Environment("it", new JdbcTransactionFactory(), source));
+      config.addInterceptor(new IoTDBQueryInterceptor());
+      GlobalConfigUtils.setGlobalConfig(
+          config,
+          new GlobalConfig()
+              .setDbConfig(new GlobalConfig.DbConfig())
+              .setSqlInjector(new IoTDBMybatisConfiguration().ioTDBSqlInjector()));
+      try (InputStream xml =
+          Files.newInputStream(output.resolve("resources/mappers/ReadingsMapper.xml"))) {
+        new XMLMapperBuilder(xml, config, "ReadingsMapper.xml", config.getSqlFragments()).parse();
+      }
+      try (SqlSession session =
+          new MybatisSqlSessionFactoryBuilder().build(config).openSession(true)) {
+        Object instance = session.getMapper(mapper);
+        Object row = entity.getConstructor().newInstance();
+        entity.getMethod("setTime", Long.class).invoke(row, timestamp() + 10);
+        entity.getMethod("setOrder", String.class).invoke(row, "quoted");
+        entity.getMethod("setTemperature", Float.class).invoke(row, 12.5f);
+        entity.getMethod("setReadingDate", LocalDate.class).invoke(row, LocalDate.of(2024, 2, 29));
+        byte[] payload = new byte[] {0, 39, (byte) 255};
+        entity.getMethod("setPayload", byte[].class).invoke(row, (Object) payload);
+        mapper.getMethod("insert", Object.class).invoke(instance, row);
+        Object actual = mapper.getMethod("selectByKey", Object.class).invoke(instance, row);
+        assertThat(entity.getMethod("getTime").invoke(actual)).isEqualTo(timestamp() + 10);
+        assertThat((byte[]) entity.getMethod("getPayload").invoke(actual)).containsExactly(payload);
+        assertThat(entity.getMethod("getReadingDate").invoke(actual))
+            .isEqualTo(LocalDate.of(2024, 2, 29));
+        entity.getMethod("setTemperature", Float.class).invoke(row, 44.5f);
+        mapper.getMethod("upsertFields", Object.class).invoke(instance, row);
+        entity.getMethod("setDescription", String.class).invoke(row, "generated");
+        mapper.getMethod("updateAttributes", Object.class).invoke(instance, row);
+        actual = mapper.getMethod("selectByKey", Object.class).invoke(instance, row);
+        assertThat(entity.getMethod("getTemperature").invoke(actual)).isEqualTo(44.5f);
+        assertThat(entity.getMethod("getDescription").invoke(actual)).isEqualTo("generated");
+        mapper.getMethod("deleteByKey", Object.class).invoke(instance, row);
+        assertThat(mapper.getMethod("selectByKey", Object.class).invoke(instance, row)).isNull();
+      }
+    } finally {
+      Resources.setDefaultClassLoader(previous);
+    }
+  }
+}
diff --git a/examples/pom.xml b/examples/pom.xml
index 13a72c5..a3ce42c 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -81,8 +81,8 @@
                     <groupId>org.apache.maven.plugins</groupId>
                     <artifactId>maven-compiler-plugin</artifactId>
                     <configuration>
-                        <source>8</source>
-                        <target>8</target>
+                        <source>17</source>
+                        <target>17</target>
                     </configuration>
                 </plugin>
                 <plugin>
diff --git a/examples/rabbitmq/readme.md b/examples/rabbitmq/readme.md
index a53ddf6..3229356 100644
--- a/examples/rabbitmq/readme.md
+++ b/examples/rabbitmq/readme.md
@@ -28,7 +28,7 @@ The example is to show how to send data from localhost to IoTDB through RabbitMQ
 
 |          | Version |
 |----------|---------|
-| IoTDB    | 2.0.5   |  
+| IoTDB    | 2.0.11   |
 | RabbitMQ | 5.26.0  |
 
 ### Dependencies with Maven
@@ -38,7 +38,7 @@ The example is to show how to send data from localhost to IoTDB through RabbitMQ
     <dependency>
         <groupId>org.apache.iotdb</groupId>
         <artifactId>iotdb-session</artifactId>
-        <version>2.0.5</version>
+        <version>2.0.11</version>
     </dependency>
     <dependency>
         <groupId>com.rabbitmq</groupId>
@@ -139,4 +139,3 @@ Step 1: Run `RabbitMQProducer.java`
 Step 2: Run `RabbitMQConsumer.java`
 
 > This class consumes data from RabbitMQ and sends the data to IoTDB-table.
-
diff --git a/examples/rocketmq/readme.md b/examples/rocketmq/readme.md
index b9a8bb8..84e9bfe 100644
--- a/examples/rocketmq/readme.md
+++ b/examples/rocketmq/readme.md
@@ -49,7 +49,7 @@ Producers insert IoTDB insert statements into partitions according to devices, e
 
 |          | Version |
 |----------|---------|
-| IoTDB    | 2.0.5   |  
+| IoTDB    | 2.0.11   |
 | RocketMQ | 5.3.3   |
 
 ### Dependencies with Maven
@@ -59,7 +59,7 @@ Producers insert IoTDB insert statements into partitions according to devices, e
     <dependency>
         <groupId>org.apache.iotdb</groupId>
         <artifactId>iotdb-session</artifactId>
-        <version>2.0.5</version>
+        <version>2.0.11</version>
     </dependency>
     <dependency>
         <groupId>org.apache.rocketmq</groupId>
diff --git a/iotdb-collector/collector-core/pom.xml b/iotdb-collector/collector-core/pom.xml
index 205fbec..5fd92f7 100644
--- a/iotdb-collector/collector-core/pom.xml
+++ b/iotdb-collector/collector-core/pom.xml
@@ -30,6 +30,10 @@
     <name>IoTDB: Collector: Core</name>
     <version>2.0.4-SNAPSHOT</version>
     <dependencies>
+        <dependency>
+            <groupId>org.apache.iotdb</groupId>
+            <artifactId>iotdb-subscription</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.apache.iotdb</groupId>
             <artifactId>collector-openapi</artifactId>
@@ -113,10 +117,6 @@
             <groupId>org.apache.iotdb</groupId>
             <artifactId>iotdb-thrift</artifactId>
         </dependency>
-        <dependency>
-            <groupId>org.apache.iotdb</groupId>
-            <artifactId>iotdb-session</artifactId>
-        </dependency>
         <dependency>
             <groupId>org.apache.thrift</groupId>
             <artifactId>libthrift</artifactId>
@@ -161,6 +161,11 @@
             <groupId>org.apache.kafka</groupId>
             <artifactId>kafka-clients</artifactId>
         </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
     <build>
         <resources>
@@ -193,8 +198,8 @@
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-compiler-plugin</artifactId>
                 <configuration>
-                    <source>8</source>
-                    <target>8</target>
+                    <source>17</source>
+                    <target>17</target>
                 </configuration>
             </plugin>
             <plugin>
diff --git a/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/plugin/builtin/sink/event/PipeRawTabletInsertionEvent.java b/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/plugin/builtin/sink/event/PipeRawTabletInsertionEvent.java
index bee813a..d95face 100644
--- a/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/plugin/builtin/sink/event/PipeRawTabletInsertionEvent.java
+++ b/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/plugin/builtin/sink/event/PipeRawTabletInsertionEvent.java
@@ -21,6 +21,7 @@ package org.apache.iotdb.collector.plugin.builtin.sink.event;
 
 import org.apache.iotdb.pipe.api.access.Row;
 import org.apache.iotdb.pipe.api.collector.RowCollector;
+import org.apache.iotdb.pipe.api.collector.TabletCollector;
 import org.apache.iotdb.pipe.api.event.dml.insertion.TabletInsertionEvent;
 
 import org.apache.tsfile.write.record.Tablet;
@@ -52,6 +53,13 @@ public class PipeRawTabletInsertionEvent extends PipeInsertionEvent
     throw new UnsupportedOperationException();
   }
 
+  @Override
+  public Iterable<TabletInsertionEvent> processTabletWithCollect(
+      final BiConsumer<Tablet, TabletCollector> consumer) {
+    // Like the row-based callbacks above, processor-side collection is not implemented here.
+    throw new UnsupportedOperationException();
+  }
+
   public Tablet getTablet() {
     return tablet;
   }
diff --git a/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/plugin/builtin/source/iotdb/IoTDBPushSource.java b/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/plugin/builtin/source/iotdb/IoTDBPushSource.java
index 7b97703..978b4a0 100644
--- a/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/plugin/builtin/source/iotdb/IoTDBPushSource.java
+++ b/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/plugin/builtin/source/iotdb/IoTDBPushSource.java
@@ -25,15 +25,21 @@ import org.apache.iotdb.pipe.api.customizer.configuration.PipeSourceRuntimeConfi
 import org.apache.iotdb.pipe.api.customizer.parameter.PipeParameterValidator;
 import org.apache.iotdb.pipe.api.customizer.parameter.PipeParameters;
 import org.apache.iotdb.rpc.subscription.config.ConsumerConstant;
+import org.apache.iotdb.rpc.subscription.config.TopicConstant;
+import org.apache.iotdb.session.subscription.ISubscriptionTreeSession;
+import org.apache.iotdb.session.subscription.SubscriptionTreeSessionBuilder;
 import org.apache.iotdb.session.subscription.consumer.tree.SubscriptionTreePullConsumer;
+import org.apache.iotdb.session.subscription.model.Topic;
 import org.apache.iotdb.session.subscription.payload.SubscriptionMessage;
 import org.apache.iotdb.session.subscription.payload.SubscriptionMessageType;
-import org.apache.iotdb.session.subscription.payload.SubscriptionSessionDataSet;
 
+import org.apache.tsfile.write.record.Tablet;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.Iterator;
 import java.util.List;
+import java.util.Locale;
 import java.util.Optional;
 import java.util.Properties;
 
@@ -48,6 +54,7 @@ public class IoTDBPushSource extends PushSource {
   private String deviceId;
 
   private volatile boolean isStarted = true;
+  private SubscriptionTreePullConsumer consumer;
   private Thread workerThread;
 
   @Override
@@ -76,42 +83,97 @@ public class IoTDBPushSource extends PushSource {
 
   @Override
   public void start() throws Exception {
-    if (workerThread == null || !workerThread.isAlive()) {
-      isStarted = true;
-      workerThread = new Thread(this::doWork);
-      workerThread.start();
+    if (workerThread != null && workerThread.isAlive()) {
+      return;
     }
-  }
 
-  private void doWork() {
+    // Validate the topic and subscribe on the calling thread so that a missing topic, a
+    // tsfile-format topic, an unreachable broker or a server without subscription support fails
+    // task creation with the cause, instead of leaving a task that looks alive but never delivers.
+    requireRecordFormatTopic();
+
     final Properties pullProperties = new Properties();
     pullProperties.put(IoTDBPushSourceConstant.HOST_KEY, host);
     pullProperties.put(IoTDBPushSourceConstant.PORT_KEY, port);
     pullProperties.put(ConsumerConstant.CONSUMER_ID_KEY, "r1");
     pullProperties.put(ConsumerConstant.CONSUMER_GROUP_ID_KEY, "rg1");
 
-    try (final SubscriptionTreePullConsumer consumer =
-        new SubscriptionTreePullConsumer(pullProperties)) {
-      consumer.open();
-      consumer.subscribe(topic);
+    final SubscriptionTreePullConsumer pullConsumer =
+        new SubscriptionTreePullConsumer(pullProperties);
+    try {
+      pullConsumer.open();
+      pullConsumer.subscribe(topic);
+    } catch (final Exception e) {
+      try {
+        pullConsumer.close();
+      } catch (final Exception closeException) {
+        e.addSuppressed(closeException);
+      }
+      throw e;
+    }
+
+    consumer = pullConsumer;
+    isStarted = true;
+    workerThread = new Thread(this::doWork, "iotdb-push-source-" + topic);
+    workerThread.start();
+  }
+
+  private void requireRecordFormatTopic() throws Exception {
+    try (final ISubscriptionTreeSession session =
+        new SubscriptionTreeSessionBuilder().host(host).port(port).build()) {
+      session.open();
+      final Optional<Topic> found = session.getTopic(topic);
+      if (!found.isPresent()) {
+        throw new IllegalArgumentException(
+            String.format(
+                "Topic %s does not exist on %s:%d; create it with format=%s before starting the"
+                    + " collector IoTDB source",
+                topic, host, port, TopicConstant.FORMAT_RECORD_HANDLER_VALUE));
+      }
+      final String attributes = String.valueOf(found.get().getTopicAttributes());
+      if (attributes.toLowerCase(Locale.ROOT).contains("tsfilehandler")) {
+        throw new IllegalArgumentException(
+            String.format(
+                "Topic %s delivers tsfile messages (%s); the collector IoTDB source only consumes"
+                    + " record-format messages, create the topic with format=%s",
+                topic, attributes, TopicConstant.FORMAT_RECORD_HANDLER_VALUE));
+      }
+    }
+  }
 
+  private void doWork() {
+    try (final SubscriptionTreePullConsumer pullConsumer = consumer) {
       while (isStarted && !Thread.currentThread().isInterrupted()) {
         markPausePosition();
 
-        final List<SubscriptionMessage> messages = consumer.poll(timeout);
+        final List<SubscriptionMessage> messages = pullConsumer.poll(timeout);
         for (final SubscriptionMessage message : messages) {
           final short messageType = message.getMessageType();
-          if (SubscriptionMessageType.isValidatedMessageType(messageType)) {
-            for (final SubscriptionSessionDataSet dataSet : message.getSessionDataSetsHandler()) {
-              final SubDemoEvent event = new SubDemoEvent(dataSet.getTablet(), deviceId);
-              supply(event);
+          if (messageType == SubscriptionMessageType.RECORD_HANDLER.getType()) {
+            final Iterator<Tablet> tablets = message.getRecordTabletIterator();
+            while (tablets.hasNext()) {
+              supply(new SubDemoEvent(tablets.next(), deviceId));
             }
+          } else if (messageType != SubscriptionMessageType.WATERMARK.getType()) {
+            throw new UnsupportedOperationException(
+                String.format(
+                    "Topic %s delivered a message of type %d; the collector IoTDB source only"
+                        + " consumes record-format messages (format=%s)",
+                    topic, messageType, TopicConstant.FORMAT_RECORD_HANDLER_VALUE));
           }
         }
       }
     } catch (final Exception e) {
       Thread.currentThread().interrupt();
-      LOGGER.error("Error in push source", e);
+      if (isStarted) {
+        LOGGER.error(
+            "The collector IoTDB source for topic {} stopped consuming; drop and recreate the task"
+                + " after fixing the cause",
+            topic,
+            e);
+      } else {
+        LOGGER.info("The collector IoTDB source for topic {} stopped", topic);
+      }
     }
   }
 
diff --git a/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/runtime/task/Task.java b/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/runtime/task/Task.java
index 2783add..9d31b5c 100644
--- a/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/runtime/task/Task.java
+++ b/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/runtime/task/Task.java
@@ -21,10 +21,21 @@ package org.apache.iotdb.collector.runtime.task;
 
 import org.apache.iotdb.pipe.api.customizer.parameter.PipeParameters;
 
+import com.lmax.disruptor.TimeoutException;
+import com.lmax.disruptor.dsl.Disruptor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
 
 public abstract class Task {
 
+  private static final Logger LOGGER = LoggerFactory.getLogger(Task.class);
+
+  private static final long WORKER_SHUTDOWN_TIMEOUT_MS = 10_000L;
+
   protected final String taskId;
   protected final PipeParameters parameters;
 
@@ -72,4 +83,67 @@ public abstract class Task {
   }
 
   public abstract void dropInternal() throws Exception;
+
+  /** Lets the workers of this stage run to completion; {@link #drop()} does the same first. */
+  final synchronized void markDropped() {
+    dispatch.remove();
+  }
+
+  /**
+   * Stops the workers of a started or never-started disruptor and the executor they run on. The
+   * backlog is drained for a bounded time; afterwards the workers are halted until the executor has
+   * terminated. Disruptor 3.x discards a halt that reaches a worker before it enters {@code run()},
+   * and that worker then parks forever, so one halt right after a quick creation failure is not
+   * enough.
+   */
+  protected static void stopWorkers(final Disruptor<?> disruptor, final ExecutorService executor) {
+    try {
+      disruptor.shutdown(WORKER_SHUTDOWN_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+    } catch (final TimeoutException e) {
+      LOGGER.warn("Discarding events not consumed within {} ms", WORKER_SHUTDOWN_TIMEOUT_MS);
+    }
+    if (executor == null) {
+      disruptor.halt();
+      return;
+    }
+    executor.shutdown();
+    if (!awaitTermination(executor, disruptor::halt)) {
+      LOGGER.warn("Workers did not stop within {} ms", WORKER_SHUTDOWN_TIMEOUT_MS);
+    }
+  }
+
+  /** Shuts the executor down and waits a bounded time for the tasks it runs to return. */
+  protected static void awaitWorkers(final ExecutorService executor) {
+    executor.shutdown();
+    if (!awaitTermination(executor, () -> {})) {
+      LOGGER.warn("Workers did not stop within {} ms", WORKER_SHUTDOWN_TIMEOUT_MS);
+    }
+  }
+
+  private static boolean awaitTermination(
+      final ExecutorService executor, final Runnable beforeEachWait) {
+    final long deadline =
+        System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(WORKER_SHUTDOWN_TIMEOUT_MS);
+    boolean interrupted = false;
+    try {
+      while (true) {
+        beforeEachWait.run();
+        try {
+          if (executor.awaitTermination(10, TimeUnit.MILLISECONDS)) {
+            return true;
+          }
+        } catch (final InterruptedException e) {
+          // Keep waiting: giving up early would leave the workers running.
+          interrupted = true;
+        }
+        if (System.nanoTime() - deadline >= 0) {
+          return false;
+        }
+      }
+    } finally {
+      if (interrupted) {
+        Thread.currentThread().interrupt();
+      }
+    }
+  }
 }
diff --git a/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/runtime/task/TaskCombiner.java b/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/runtime/task/TaskCombiner.java
index c835bc9..4506e7e 100644
--- a/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/runtime/task/TaskCombiner.java
+++ b/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/runtime/task/TaskCombiner.java
@@ -37,9 +37,23 @@ public class TaskCombiner {
   }
 
   public void create() throws Exception {
-    sink.create();
-    processor.create();
-    source.create();
+    try {
+      sink.create();
+      processor.create();
+      source.create();
+    } catch (final Exception | Error e) {
+      // A task that fails creation is never registered, so nothing would drop it later. Release
+      // what every stage already holds; a leaked executor would otherwise be reused by a retry
+      // under the same task id while its threads still run this attempt's workers.
+      for (final Task task : new Task[] {source, processor, sink}) {
+        try {
+          task.drop();
+        } catch (final Exception | Error dropException) {
+          e.addSuppressed(dropException);
+        }
+      }
+      throw e;
+    }
   }
 
   public void start() throws Exception {
@@ -57,6 +71,12 @@ public class TaskCombiner {
   public void drop() throws Exception {
     stop();
 
+    // Mark every stage dropped before draining any: a paused downstream stage would not consume
+    // what the stage upstream of it drains, and each drain would wait for its timeout instead.
+    source.markDropped();
+    processor.markDropped();
+    sink.markDropped();
+
     source.drop();
     processor.drop();
     sink.drop();
diff --git a/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/runtime/task/processor/ProcessorTask.java b/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/runtime/task/processor/ProcessorTask.java
index 03353a7..11cc9a0 100644
--- a/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/runtime/task/processor/ProcessorTask.java
+++ b/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/runtime/task/processor/ProcessorTask.java
@@ -102,21 +102,14 @@ public class ProcessorTask extends Task {
       processorConsumers[i] =
           new ProcessorConsumer(pluginRuntime.constructProcessor(parameters), sinkProducer);
       processorConsumers[i].setDispatch(dispatch);
-      try {
-        processorConsumers[i].consumer().validate(new PipeParameterValidator(parameters));
-        processorConsumers[i]
-            .consumer()
-            .customize(
-                parameters,
-                new CollectorProcessorRuntimeConfiguration(taskId, creationTime, parallelism, i));
-      } catch (final Exception e) {
-        try {
-          processorConsumers[i].consumer().close();
-        } catch (final Exception ex) {
-          LOGGER.warn("Failed to close sink on creation failure", ex);
-        }
-        throw e;
-      }
+      // A failure propagates; TaskCombiner then drops the task, closing every constructed
+      // processor.
+      processorConsumers[i].consumer().validate(new PipeParameterValidator(parameters));
+      processorConsumers[i]
+          .consumer()
+          .customize(
+              parameters,
+              new CollectorProcessorRuntimeConfiguration(taskId, creationTime, parallelism, i));
     }
     disruptor.handleEventsWithWorkerPool(processorConsumers);
 
@@ -148,25 +141,26 @@ public class ProcessorTask extends Task {
 
   @Override
   public void dropInternal() {
+    // Stop the workers before closing the processors they call, and before deregistering the
+    // heartbeat job, which throws when the schedule service has not started yet.
+    stopWorkers(disruptor, REGISTERED_EXECUTOR_SERVICES.remove(taskId));
+
     if (processorConsumers != null) {
       for (int i = 0; i < parallelism; i++) {
+        // Slots after a creation failure were never constructed.
+        if (processorConsumers[i] == null) {
+          continue;
+        }
         try {
           processorConsumers[i].consumer().close();
         } catch (final Exception e) {
-          LOGGER.warn("Failed to close sink", e);
+          LOGGER.warn("Failed to close processor", e);
         }
       }
     }
 
     // remove proactive sink actions
     ScheduleService.pushEvent().ifPresent(event -> event.deregister(taskId));
-
-    disruptor.shutdown();
-
-    final ExecutorService executorService = REGISTERED_EXECUTOR_SERVICES.remove(taskId);
-    if (executorService != null) {
-      executorService.shutdown();
-    }
   }
 
   public EventCollector makeProducer() {
diff --git a/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/runtime/task/sink/SinkTask.java b/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/runtime/task/sink/SinkTask.java
index 3def6d9..cc9cbc9 100644
--- a/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/runtime/task/sink/SinkTask.java
+++ b/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/runtime/task/sink/SinkTask.java
@@ -93,22 +93,14 @@ public class SinkTask extends Task {
     for (int i = 0; i < parallelism; i++) {
       consumers[i] = new SinkConsumer(pluginRuntime.constructSink(parameters));
       consumers[i].setDispatch(dispatch);
-      try {
-        consumers[i].consumer().validate(new PipeParameterValidator(parameters));
-        consumers[i]
-            .consumer()
-            .customize(
-                parameters,
-                new CollectorSinkRuntimeConfiguration(taskId, creationTime, parallelism, i));
-        consumers[i].consumer().handshake();
-      } catch (final Exception e) {
-        try {
-          consumers[i].consumer().close();
-        } catch (final Exception ex) {
-          LOGGER.warn("Failed to close sink on creation failure", ex);
-        }
-        throw e;
-      }
+      // A failure propagates; TaskCombiner then drops the task, closing every constructed sink.
+      consumers[i].consumer().validate(new PipeParameterValidator(parameters));
+      consumers[i]
+          .consumer()
+          .customize(
+              parameters,
+              new CollectorSinkRuntimeConfiguration(taskId, creationTime, parallelism, i));
+      consumers[i].consumer().handshake();
     }
     disruptor.handleEventsWithWorkerPool(consumers);
 
@@ -129,8 +121,15 @@ public class SinkTask extends Task {
 
   @Override
   public void dropInternal() {
+    // Stop the workers before closing the sinks they call.
+    stopWorkers(disruptor, REGISTERED_EXECUTOR_SERVICES.remove(taskId));
+
     if (consumers != null) {
       for (int i = 0; i < parallelism; i++) {
+        // Slots after a creation failure were never constructed.
+        if (consumers[i] == null) {
+          continue;
+        }
         try {
           consumers[i].consumer().close();
         } catch (final Exception e) {
@@ -138,13 +137,6 @@ public class SinkTask extends Task {
         }
       }
     }
-
-    disruptor.shutdown();
-
-    final ExecutorService executorService = REGISTERED_EXECUTOR_SERVICES.remove(taskId);
-    if (executorService != null) {
-      executorService.shutdown();
-    }
   }
 
   public EventCollector makeProducer() {
diff --git a/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/runtime/task/source/pull/PullSourceTask.java b/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/runtime/task/source/pull/PullSourceTask.java
index 5cf7468..b17f560 100644
--- a/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/runtime/task/source/pull/PullSourceTask.java
+++ b/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/runtime/task/source/pull/PullSourceTask.java
@@ -86,22 +86,16 @@ public class PullSourceTask extends SourceTask {
       consumers[i] =
           new PullSourceConsumer(
               (PullSource) pluginRuntime.constructSource(parameters), processorProducer);
-      try {
-        consumers[i].consumer().validate(new PipeParameterValidator(parameters));
-        consumers[i]
-            .consumer()
-            .customize(
-                parameters,
-                new CollectorSourceRuntimeConfiguration(taskId, creationTime, parallelism, i));
-        consumers[i].consumer().start();
-      } catch (final Exception e) {
-        try {
-          consumers[i].consumer().close();
-        } catch (final Exception ex) {
-          LOGGER.warn("Failed to close source on creation failure", ex);
-          throw e;
-        }
-      }
+      // A failure propagates so that task creation fails; TaskCombiner then drops the task, which
+      // stops the loops already submitted, closes every constructed source and releases the
+      // executor.
+      consumers[i].consumer().validate(new PipeParameterValidator(parameters));
+      consumers[i]
+          .consumer()
+          .customize(
+              parameters,
+              new CollectorSourceRuntimeConfiguration(taskId, creationTime, parallelism, i));
+      consumers[i].consumer().start();
 
       int finalI = i;
       REGISTERED_EXECUTOR_SERVICES
@@ -171,8 +165,19 @@ public class PullSourceTask extends SourceTask {
 
   @Override
   public void dropInternal() {
+    // The dropped dispatch ends every polling loop after its current supply(); wait for that
+    // before closing the sources those loops call.
+    final ExecutorService executorService = REGISTERED_EXECUTOR_SERVICES.remove(taskId);
+    if (executorService != null) {
+      awaitWorkers(executorService);
+    }
+
     if (consumers != null) {
       for (int i = 0; i < parallelism; i++) {
+        // Slots after a creation failure were never constructed.
+        if (consumers[i] == null) {
+          continue;
+        }
         try {
           consumers[i].consumer().close();
         } catch (final Exception e) {
@@ -180,10 +185,5 @@ public class PullSourceTask extends SourceTask {
         }
       }
     }
-
-    final ExecutorService executorService = REGISTERED_EXECUTOR_SERVICES.remove(taskId);
-    if (executorService != null) {
-      executorService.shutdown();
-    }
   }
 }
diff --git a/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/runtime/task/source/push/PushSourceTask.java b/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/runtime/task/source/push/PushSourceTask.java
index 1c74234..4b9d085 100644
--- a/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/runtime/task/source/push/PushSourceTask.java
+++ b/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/runtime/task/source/push/PushSourceTask.java
@@ -67,23 +67,16 @@ public class PushSourceTask extends SourceTask {
       pushSources[i] = (PushSource) pluginRuntime.constructSource(parameters);
       pushSources[i].setCollector(processorProducer);
       pushSources[i].setDispatch(dispatch);
-      try {
-        pushSources[i].validate(new PipeParameterValidator(parameters));
-        pushSources[i].customize(
-            parameters,
-            new CollectorSourceRuntimeConfiguration(taskId, creationTime, parallelism, i));
-        if (TaskStateEnum.STOPPED.equals(taskState)) {
-          pushSources[i].pause();
-        }
-        pushSources[i].start();
-      } catch (final Exception e) {
-        try {
-          pushSources[i].close();
-        } catch (final Exception ex) {
-          LOGGER.warn("Failed to close source on creation failure", ex);
-          throw e;
-        }
+      // A failure propagates so that task creation fails; TaskCombiner then drops the task, which
+      // closes every source constructed so far, including those already started.
+      pushSources[i].validate(new PipeParameterValidator(parameters));
+      pushSources[i].customize(
+          parameters,
+          new CollectorSourceRuntimeConfiguration(taskId, creationTime, parallelism, i));
+      if (TaskStateEnum.STOPPED.equals(taskState)) {
+        pushSources[i].pause();
       }
+      pushSources[i].start();
     }
 
     // register storage progress schedule job
@@ -158,6 +151,10 @@ public class PushSourceTask extends SourceTask {
   public void dropInternal() {
     if (pushSources != null) {
       for (int i = 0; i < parallelism; i++) {
+        // Slots after a creation failure were never constructed.
+        if (pushSources[i] == null) {
+          continue;
+        }
         try {
           pushSources[i].close();
         } catch (final Exception e) {
diff --git a/iotdb-collector/collector-core/src/test/java/org/apache/iotdb/collector/runtime/task/TaskCombinerTest.java b/iotdb-collector/collector-core/src/test/java/org/apache/iotdb/collector/runtime/task/TaskCombinerTest.java
new file mode 100644
index 0000000..d021554
--- /dev/null
+++ b/iotdb-collector/collector-core/src/test/java/org/apache/iotdb/collector/runtime/task/TaskCombinerTest.java
@@ -0,0 +1,417 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.collector.runtime.task;
+
+import org.apache.iotdb.collector.config.TaskRuntimeOptions;
+import org.apache.iotdb.collector.plugin.api.PullSource;
+import org.apache.iotdb.collector.plugin.api.PushSource;
+import org.apache.iotdb.collector.plugin.builtin.processor.DoNothingProcessor;
+import org.apache.iotdb.collector.runtime.plugin.PluginRuntime;
+import org.apache.iotdb.collector.runtime.progress.ProgressIndex;
+import org.apache.iotdb.collector.runtime.task.processor.ProcessorTask;
+import org.apache.iotdb.collector.runtime.task.sink.SinkTask;
+import org.apache.iotdb.collector.runtime.task.source.SourceTask;
+import org.apache.iotdb.collector.runtime.task.source.pull.PullSourceTask;
+import org.apache.iotdb.collector.runtime.task.source.push.PushSourceTask;
+import org.apache.iotdb.collector.service.RuntimeService;
+import org.apache.iotdb.collector.service.ScheduleService;
+import org.apache.iotdb.pipe.api.PipeProcessor;
+import org.apache.iotdb.pipe.api.PipeSink;
+import org.apache.iotdb.pipe.api.PipeSource;
+import org.apache.iotdb.pipe.api.customizer.configuration.PipeConnectorRuntimeConfiguration;
+import org.apache.iotdb.pipe.api.customizer.configuration.PipeSinkRuntimeConfiguration;
+import org.apache.iotdb.pipe.api.customizer.configuration.PipeSourceRuntimeConfiguration;
+import org.apache.iotdb.pipe.api.customizer.parameter.PipeParameterValidator;
+import org.apache.iotdb.pipe.api.customizer.parameter.PipeParameters;
+import org.apache.iotdb.pipe.api.event.Event;
+import org.apache.iotdb.pipe.api.event.dml.insertion.TabletInsertionEvent;
+import org.apache.iotdb.pipe.api.event.dml.insertion.TsFileInsertionEvent;
+
+import com.lmax.disruptor.WorkProcessor;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class TaskCombinerTest {
+
+  private final ScheduleService scheduleService = new ScheduleService();
+  private final FakePlugins plugins = new FakePlugins();
+  private PluginRuntime previousPlugins;
+
+  @Before
+  public void setUp() throws Exception {
+    scheduleService.start();
+    previousPlugins = pluginRuntime().getAndSet(plugins);
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    pluginRuntime().set(previousPlugins);
+    scheduleService.stop();
+  }
+
+  @Test
+  public void failedPushSourceReleasesEveryStageSoTheTaskCanBeRecreated() throws Exception {
+    final FakePushSource started = new FakePushSource(null, false);
+    final FakePushSource failing = new FakePushSource(new IllegalStateException("no topic"), false);
+    plugins.sources.add(started);
+    plugins.sources.add(failing);
+
+    try {
+      combiner("push-rollback", 2, false).create();
+      fail("task creation must fail when a source cannot start");
+    } catch (final IllegalStateException e) {
+      assertEquals("no topic", e.getMessage());
+    }
+    assertTrue("a source started before the failure must be closed", started.closed);
+    assertTrue(failing.closed);
+    assertEquals(
+        TaskRuntimeOptions.TASK_SINK_PARALLELISM_NUM.value().intValue(), plugins.closedSinks.get());
+    assertNoDisruptorWorkers();
+
+    // Without the rollback, the retry reuses the failed attempt's executors while their threads
+    // still run its workers, so the event below would never reach the sink.
+    plugins.sources.add(new FakePushSource(null, true));
+    final TaskCombiner retried = combiner("push-rollback", 1, false);
+    retried.create();
+    try {
+      assertTrue(
+          "a retry under the same task id must deliver",
+          plugins.delivered.await(10, TimeUnit.SECONDS));
+    } finally {
+      retried.drop();
+    }
+    assertNoDisruptorWorkers();
+  }
+
+  @Test
+  public void failedPullSourceStopsTheLoopsAlreadyRunning() throws Exception {
+    final FakePullSource polling = new FakePullSource(null, null);
+    final FakePullSource failing =
+        new FakePullSource(new IllegalStateException("unreachable"), polling.firstPoll);
+    plugins.sources.add(polling);
+    plugins.sources.add(failing);
+
+    try {
+      combiner("pull-rollback", 2, true).create();
+      fail("task creation must fail when a source cannot start");
+    } catch (final IllegalStateException e) {
+      assertEquals("unreachable", e.getMessage());
+    }
+    assertTrue(polling.closed);
+    assertTrue(failing.closed);
+    assertNoDisruptorWorkers();
+
+    // The failing source only fails once the first loop has polled, so a loop was running; the
+    // rollback waits for it to return before closing the source.
+    final int polls = polling.polls.get();
+    assertTrue(polls > 0);
+    Thread.sleep(200);
+    assertEquals("the loop must stop polling a closed source", polls, polling.polls.get());
+  }
+
+  @Test
+  public void dropDrainsABackloggedTaskWithoutWaitingForTheTimeout() throws Exception {
+    plugins.transferDelayMs = 1;
+    final FloodingPushSource flooding = new FloodingPushSource();
+    plugins.sources.add(flooding);
+    final TaskCombiner combiner = combiner("backlog-drop", 1, false);
+    combiner.create();
+    // Both ring buffers (1024 slots each) are full behind the slow sink once this many are out.
+    assertTrue(flooding.backlogged.await(10, TimeUnit.SECONDS));
+
+    // Draining the processor used to wait for its timeout: the paused sink did not consume.
+    final long start = System.nanoTime();
+    combiner.drop();
+    final long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
+    assertTrue("drop took " + elapsedMs + " ms", elapsedMs < 5_000);
+    assertTrue(flooding.closed);
+    assertNoDisruptorWorkers();
+  }
+
+  /**
+   * A halt that reaches a disruptor worker before it runs is lost, so a quick creation failure used
+   * to leave sink and processor workers parked forever.
+   */
+  private static void assertNoDisruptorWorkers() {
+    for (final Map.Entry<Thread, StackTraceElement[]> thread :
+        Thread.getAllStackTraces().entrySet()) {
+      for (final StackTraceElement frame : thread.getValue()) {
+        assertFalse(
+            thread.getKey().getName() + " still runs a disruptor worker",
+            WorkProcessor.class.getName().equals(frame.getClassName()));
+      }
+    }
+  }
+
+  private static TaskCombiner combiner(
+      final String taskId, final int sourceParallelism, final boolean pull) {
+    final SinkTask sink = new SinkTask(taskId, new HashMap<>());
+    final ProcessorTask processor = new ProcessorTask(taskId, new HashMap<>(), sink.makeProducer());
+    final Map<String, String> sourceAttributes = new HashMap<>();
+    sourceAttributes.put(
+        TaskRuntimeOptions.TASK_SOURCE_PARALLELISM_NUM.key(), String.valueOf(sourceParallelism));
+    final SourceTask source =
+        pull
+            ? new PullSourceTask(
+                taskId, sourceAttributes, processor.makeProducer(), TaskStateEnum.RUNNING)
+            : new PushSourceTask(
+                taskId, sourceAttributes, processor.makeProducer(), TaskStateEnum.RUNNING);
+    return new TaskCombiner(source, processor, sink);
+  }
+
+  @SuppressWarnings("unchecked")
+  private static AtomicReference<PluginRuntime> pluginRuntime()
+      throws ReflectiveOperationException {
+    // RuntimeService offers no way to install a plugin runtime other than starting the service.
+    final Field field = RuntimeService.class.getDeclaredField("PLUGIN");
+    field.setAccessible(true);
+    return (AtomicReference<PluginRuntime>) field.get(null);
+  }
+
+  private static final class FakePlugins extends PluginRuntime {
+
+    private final Queue<PipeSource> sources = new ConcurrentLinkedQueue<>();
+    private final AtomicInteger closedSinks = new AtomicInteger();
+    private final CountDownLatch delivered = new CountDownLatch(1);
+    private volatile long transferDelayMs;
+
+    @Override
+    public PipeSource constructSource(final PipeParameters sourceParameters) {
+      return sources.remove();
+    }
+
+    @Override
+    public PipeProcessor constructProcessor(final PipeParameters processorParameters) {
+      return new DoNothingProcessor();
+    }
+
+    @Override
+    public PipeSink constructSink(final PipeParameters sinkParameters) {
+      return new FakeSink(this);
+    }
+  }
+
+  private static final class MarkerEvent implements Event {}
+
+  private static final class FakePushSource extends PushSource {
+
+    private final Exception startFailure;
+    private final boolean emit;
+    private volatile boolean closed;
+
+    private FakePushSource(final Exception startFailure, final boolean emit) {
+      this.startFailure = startFailure;
+      this.emit = emit;
+    }
+
+    @Override
+    public void validate(final PipeParameterValidator validator) {}
+
+    @Override
+    public void customize(
+        final PipeParameters parameters, final PipeSourceRuntimeConfiguration configuration) {}
+
+    @Override
+    public void start() throws Exception {
+      if (startFailure != null) {
+        throw startFailure;
+      }
+      if (emit) {
+        supply(new MarkerEvent());
+      }
+    }
+
+    @Override
+    public Optional<ProgressIndex> report() {
+      return Optional.empty();
+    }
+
+    @Override
+    public void close() {
+      closed = true;
+    }
+  }
+
+  private static final class FloodingPushSource extends PushSource {
+
+    private final CountDownLatch backlogged = new CountDownLatch(1);
+    private volatile boolean closed;
+    private Thread worker;
+
+    @Override
+    public void validate(final PipeParameterValidator validator) {}
+
+    @Override
+    public void customize(
+        final PipeParameters parameters, final PipeSourceRuntimeConfiguration configuration) {}
+
+    @Override
+    public void start() {
+      worker =
+          new Thread(
+              () -> {
+                try {
+                  for (int published = 1; !closed; published++) {
+                    supply(new Event() {});
+                    if (published == 3_000) {
+                      backlogged.countDown();
+                    }
+                  }
+                } catch (final Exception e) {
+                  throw new IllegalStateException(e);
+                }
+              },
+              "flooding-push-source");
+      worker.start();
+    }
+
+    @Override
+    public Optional<ProgressIndex> report() {
+      return Optional.empty();
+    }
+
+    @Override
+    public void close() throws InterruptedException {
+      closed = true;
+      worker.join(10_000);
+    }
+  }
+
+  private static final class FakePullSource extends PullSource {
+
+    private final Exception startFailure;
+    private final CountDownLatch failAfter;
+    private final CountDownLatch firstPoll = new CountDownLatch(1);
+    private final AtomicInteger polls = new AtomicInteger();
+    private volatile boolean closed;
+
+    private FakePullSource(final Exception startFailure, final CountDownLatch failAfter) {
+      this.startFailure = startFailure;
+      this.failAfter = failAfter;
+    }
+
+    @Override
+    public void validate(final PipeParameterValidator validator) {}
+
+    @Override
+    public void customize(
+        final PipeParameters parameters, final PipeSourceRuntimeConfiguration configuration) {}
+
+    @Override
+    public void start() throws Exception {
+      if (startFailure != null) {
+        failAfter.await(10, TimeUnit.SECONDS);
+        throw startFailure;
+      }
+    }
+
+    @Override
+    public Event supply() throws InterruptedException {
+      polls.incrementAndGet();
+      firstPoll.countDown();
+      Thread.sleep(5);
+      return null;
+    }
+
+    @Override
+    public Optional<ProgressIndex> report() {
+      return Optional.empty();
+    }
+
+    @Override
+    public void close() {
+      closed = true;
+    }
+  }
+
+  private static final class FakeSink implements PipeSink {
+
+    private final FakePlugins plugins;
+
+    private FakeSink(final FakePlugins plugins) {
+      this.plugins = plugins;
+    }
+
+    @Override
+    public void validate(final PipeParameterValidator validator) {}
+
+    // Both customize overloads are abstract in the pipe API. Every overload below forwards to a
+    // single implementation, so which one the argument's static type selects does not matter.
+    @Override
+    public void customize(
+        final PipeParameters parameters, final PipeConnectorRuntimeConfiguration configuration) {}
+
+    @Override
+    public void customize(
+        final PipeParameters parameters, final PipeSinkRuntimeConfiguration configuration) {
+      customize(parameters, (PipeConnectorRuntimeConfiguration) configuration);
+    }
+
+    @Override
+    public void handshake() {}
+
+    @Override
+    public void heartbeat() {}
+
+    @Override
+    public void transfer(final TabletInsertionEvent tabletInsertionEvent)
+        throws InterruptedException {
+      transfer((Event) tabletInsertionEvent);
+    }
+
+    @Override
+    public void transfer(final TsFileInsertionEvent tsFileInsertionEvent)
+        throws InterruptedException {
+      transfer((Event) tsFileInsertionEvent);
+    }
+
+    @Override
+    public void transfer(final Event event) throws InterruptedException {
+      if (plugins.transferDelayMs > 0) {
+        Thread.sleep(plugins.transferDelayMs);
+      }
+      if (event instanceof MarkerEvent) {
+        plugins.delivered.countDown();
+      }
+    }
+
+    @Override
+    public void close() {
+      plugins.closedSinks.incrementAndGet();
+    }
+  }
+}
diff --git a/iotdb-spring-boot-starter/README.md b/iotdb-spring-boot-starter/README.md
index d3ab8af..8894c98 100644
--- a/iotdb-spring-boot-starter/README.md
+++ b/iotdb-spring-boot-starter/README.md
@@ -19,47 +19,150 @@
 
 -->
 
-# iotdb-spring-boot-starter
+# IoTDB Spring Boot Starter
 
-- After 'clone' the project, execute 'mvn clean install'. This step is not necessary as it has already been uploaded to the Maven central repository
+Auto-configures the native IoTDB tree and table session pools. This is a Session API integration; JDBC/MyBatis applications use `iotdb-jdbc` and a JDBC data source instead.
 
-- Add the following configuration to the 'pom' file of the project to be generated:
+## Compatibility and build
 
+- JDK 17 or newer; CI builds on JDK 17 and 21.
+- Default build line: Spring Boot 3.5.1 / Spring Framework 6.2.8.
+- Spring Boot 4.1.1 / Spring Framework 7.0.9 is validated in CI.
+- IoTDB Java client 2.0.11, with TsFile 2.4.0.
+- The Extras artifact version remains `2.0.4-SNAPSHOT`. It is independent of the IoTDB server/client version; do not assume a `2.0.11` starter artifact exists.
+
+From the repository root, install the starter and its parent locally:
+
+```sh
+mvn -Pwith-springboot -pl iotdb-spring-boot-starter -am clean install
+```
+
+Use the locally built artifact (or the version of Extras you have actually published):
+
+```xml
+<dependency>
+    <groupId>org.apache.iotdb</groupId>
+    <artifactId>iotdb-spring-boot-starter</artifactId>
+    <version>2.0.4-SNAPSHOT</version>
+</dependency>
 ```
-    <dependencies>
-        <dependency>
-            <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-test</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.iotdb</groupId>
-            <artifactId>iotdb-spring-boot-starter</artifactId>
-            <version>2.0.3</version>
-        </dependency>
-    </dependencies>
+
+The configuration processor is optional and is not a runtime dependency of consuming applications.
+
+The same starter artifact supports both Spring Boot lines. Applications should use their own Spring
+Boot parent or BOM so Spring dependencies resolve to the application's chosen Boot version. To run
+the repository compatibility test for Boot 4:
+
+```sh
+mvn -Pwith-springboot,spring-boot4 -pl iotdb-spring-boot-starter -am clean test
 ```
 
-- Use The target Bean with @Autowired like:
+## Configure
+
+```properties
+iotdb.session.node-urls=127.0.0.1:6667;127.0.0.1:6668
+iotdb.session.username=${IOTDB_USERNAME:root}
+iotdb.session.password=${IOTDB_PASSWORD:root}
+iotdb.session.database=wind
+iotdb.session.max-size=10
+iotdb.session.connection-timeout-in-ms=5000
+iotdb.session.query-timeout-in-ms=60000
+```
+
+Create the `wind` database and your table separately before issuing queries. The starter does not create schema.
+
+Defaults below are checked against IoTDB 2.0.11. Unless marked otherwise, a property applies to both pools.
+
+| Property under `iotdb.session` | Type | Default | Purpose |
+|---|---|---|---|
+| `node-urls` | String | `127.0.0.1:6667` | Semicolon-separated RPC endpoints; surrounding whitespace is trimmed |
+| `username` | String | `root` | Client username |
+| `password` | String | `root` | Client password |
+| `database` | String | unset | Default database for the table pool only |
+| `max-size` | Integer | `5` | Maximum sessions in each pool |
+| `fetch-size` | Integer | `5000` | Rows per query batch, from `SessionConfig.DEFAULT_FETCH_SIZE` |
+| `connection-timeout-in-ms` | Integer | `0` | Connection timeout in milliseconds; `0` means no timeout |
+| `query-timeout-in-ms` | Long | `60000` | Query timeout in milliseconds; negative uses the server default, `0` disables the timeout |
+| `wait-to-get-session-timeout-in-ms` | Long | `60000` | Pool acquisition timeout in milliseconds |
+| `max-retry-count` | Integer | `60` | Connection retry limit; `0` disables retries |
+| `retry-interval-in-ms` | Long | `500` | Delay between connection retries in milliseconds |
+| `enable-auto-fetch` | Boolean | `true` | Refresh available DataNode endpoints in the background |
+| `enable-compression` | Boolean | `false` | Enable Thrift compact protocol; match the server configuration |
+| `use-ssl` | Boolean | `false` | Enable TLS |
+| `trust-store` | String | unset | Trust store path for TLS connections |
+| `trust-store-pwd` | String | unset | Trust store password for TLS connections |
+| `zone-id` | ZoneId | JVM default timezone | Session timezone, for example `Asia/Shanghai` or `UTC` |
+| `thrift-default-buffer-size` | Integer | `1024` | Initial Thrift buffer size in bytes |
+| `thrift-max-frame-size` | Integer | `67108864` | Maximum Thrift frame size in bytes (64 MiB) |
+| `enable-redirection` | Boolean | `false` | Redirect writes to the relevant leader; the starter retains its historical default, while the driver defaults to `true` |
+| `enable-records-auto-convert-tablet` | Boolean | `true` | Tree pool record-to-tablet conversion only |
+| `sql-dialect` | String | `table` | Deprecated compatibility property; does not select or disable a pool |
+
+The default `fetch-size` changes from `1024` to the IoTDB client default of `5000`. An explicitly
+configured `iotdb.session.fetch-size` still overrides it. The Thrift buffer default remains `1024`
+bytes; this is a separate setting.
+
+`connection-timeout-in-ms` now defaults to `0` (no timeout). Earlier starters left it unset and
+failed at startup with a `NullPointerException` unless the property was configured explicitly.
+
+`enable-compression` maps to `enableThriftCompression` for the table builder and
+`enableThriftRpcCompaction` for the tree builder. IoTDB RPC compression is a separate setting and
+retains the driver's default (`true` in 2.0.11), even when `enable-compression=false`.
+
+The starter does not expose the builders' `enableIoTDBRpcCompression`, `keyStore`, `keyStorePwd`,
+`sslProtocol`, or tree protocol `version` options as configuration properties. Supply a custom pool
+bean when those options are needed.
+
+Kebab-case, legacy underscore and camelCase names (for example `fetch-size`, `fetch_size` and
+`fetchSize`) all bind through Spring Boot relaxed binding. Use kebab-case in new configuration.
+`sql_dialect` is retained for binding compatibility but does not select or disable a pool: inject
+`ITableSessionPool` for tables or `ISessionPool` for trees.
+
+## Query and release resources
 
 ```java
-        @Autowired
-        private ITableSessionPool ioTDBSessionPool;
-
-        public void queryTableSessionPool() throws IoTDBConnectionException, StatementExecutionException {
-            ITableSession tableSession = ioTDBSessionPool.getSession();
-            final SessionDataSet sessionDataSet = tableSession.executeQueryStatement("select * from table1 limit 10");
-            while (sessionDataSet.hasNext()) {
-                final RowRecord rowRecord = sessionDataSet.next();
-                final List<Field> fields = rowRecord.getFields();
-                for (Field field : fields) {
-                    System.out.print(field.getStringValue());
-                }
-                System.out.println();
+import org.apache.iotdb.isession.ITableSession;
+import org.apache.iotdb.isession.SessionDataSet;
+import org.apache.iotdb.isession.pool.ITableSessionPool;
+import org.apache.iotdb.rpc.IoTDBConnectionException;
+import org.apache.iotdb.rpc.StatementExecutionException;
+import org.springframework.stereotype.Service;
+
+@Service
+public class Measurements {
+    private final ITableSessionPool pool;
+
+    public Measurements(ITableSessionPool pool) {
+        this.pool = pool;
+    }
+
+    public void printLatest() throws IoTDBConnectionException, StatementExecutionException {
+        try (ITableSession session = pool.getSession();
+             SessionDataSet rows = session.executeQueryStatement(
+                 "SELECT * FROM power_data_set LIMIT 10")) {
+            while (rows.hasNext()) {
+                System.out.println(rows.next());
             }
         }
+    }
+}
 ```
+
+Closing the borrowed session returns it to the pool, including on exceptions. Tree queries return a `SessionDataSetWrapper`, which must also be closed. Spring closes the pool beans when the application context shuts down; application code should not close a shared pool after each query.
+
+The starter creates `tableSessionPool` and `treeSessionPool` only when the application has not supplied a bean of the corresponding interface. A custom pool for one model does not disable the other model's default pool.
+
+This starter does not install a Spring transaction manager. IoTDB JDBC 2.0.11 `commit` and `rollback` do not provide database rollback semantics, and `@Transactional` cannot add those semantics to native Session calls.
+
+## Validation
+
+```sh
+mvn -Pwith-springboot -pl iotdb-spring-boot-starter -am test
+```
+
+The tests cover default configuration, automatic discovery, binding all 22 parameters with
+kebab-case/underscore/camelCase names, propagation into both pools, special timeout/retry values,
+invalid endpoints, custom bean backoff and context lifecycle without requiring an IoTDB server.
+They verify that both pools use the default fetch size and honor an explicit override. TLS settings
+are checked at configuration level; these tests do not perform a TLS handshake or a live query.
+See the [application example](../examples/iotdb-spring-boot-start/README.md) for a live query.
diff --git a/iotdb-spring-boot-starter/pom.xml b/iotdb-spring-boot-starter/pom.xml
index 83a75de..85552d4 100644
--- a/iotdb-spring-boot-starter/pom.xml
+++ b/iotdb-spring-boot-starter/pom.xml
@@ -35,8 +35,9 @@
         <maven.compiler.target>17</maven.compiler.target>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <spring-boot.version>3.5.1</spring-boot.version>
-        <spring.version>6.2.7</spring.version>
-        <iotdb-version>2.0.5</iotdb-version>
+        <spring-boot4.version>4.1.1</spring-boot4.version>
+        <!-- Override the root's legacy Boot 2 / Spring 5 dependency management. -->
+        <spring.version>6.2.8</spring.version>
     </properties>
     <dependencyManagement>
         <dependencies>
@@ -79,11 +80,32 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-configuration-processor</artifactId>
             <version>${spring-boot.version}</version>
+            <optional>true</optional>
         </dependency>
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-context</artifactId>
-            <version>${spring.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-test</artifactId>
+            <version>${spring-boot.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-beans</artifactId>
+            <scope>test</scope>
         </dependency>
     </dependencies>
     <build>
@@ -105,4 +127,13 @@
             </plugin>
         </plugins>
     </build>
+    <profiles>
+        <profile>
+            <id>spring-boot4</id>
+            <properties>
+                <spring-boot.version>${spring-boot4.version}</spring-boot.version>
+                <spring.version>7.0.9</spring.version>
+            </properties>
+        </profile>
+    </profiles>
 </project>
diff --git a/iotdb-spring-boot-starter/src/main/java/org/apache/iotdb/config/IoTDBSessionProperties.java b/iotdb-spring-boot-starter/src/main/java/org/apache/iotdb/config/IoTDBSessionProperties.java
index 5c2a3e0..efa26d3 100644
--- a/iotdb-spring-boot-starter/src/main/java/org/apache/iotdb/config/IoTDBSessionProperties.java
+++ b/iotdb-spring-boot-starter/src/main/java/org/apache/iotdb/config/IoTDBSessionProperties.java
@@ -20,32 +20,76 @@ package org.apache.iotdb.config;
 import org.apache.iotdb.isession.SessionConfig;
 
 import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
 
 import java.time.ZoneId;
 
 @ConfigurationProperties(prefix = "iotdb.session")
 public class IoTDBSessionProperties {
-  private String node_urls;
-  private String username;
-  private String password;
+  /** Semicolon-separated RPC endpoints in host:port form. */
+  private String node_urls = "127.0.0.1:6667";
+
+  /** Username shared by the tree and table pools. */
+  private String username = "root";
+
+  /** Password shared by the tree and table pools. */
+  private String password = "root";
+
+  /** Default database for the table pool only; does not create a database. */
   private String database;
+
+  /** Legacy property; both pool types are created regardless of this value. */
   private String sql_dialect = "table";
+
+  /** Maximum number of sessions in each pool. */
   private Integer max_size = 5;
-  private Integer fetch_size = 1024;
+
+  /** Rows fetched per query batch; defaults to the IoTDB client value (5000 in 2.0.11). */
+  private Integer fetch_size = SessionConfig.DEFAULT_FETCH_SIZE;
+
+  /** Query timeout in milliseconds; negative uses the server default, zero disables the timeout. */
   private long query_timeout_in_ms = 60000L;
+
+  /** Refresh available DataNode endpoints in the background. */
   private boolean enable_auto_fetch = true;
+
+  /** Maximum connection retries; zero disables retries. */
   private Integer max_retry_count = 60;
+
+  /** Maximum time in milliseconds to wait for a session from a full pool. */
   private long wait_to_get_session_timeout_in_ms = 60000L;
+
+  /** Enable Thrift compact protocol; independent of IoTDB RPC compression. */
   private boolean enable_compression = false;
+
+  /** Delay between connection retries in milliseconds (500 in IoTDB 2.0.11). */
   private long retry_interval_in_ms = SessionConfig.RETRY_INTERVAL_IN_MS;
+
+  /** Enable TLS for client connections. */
   private boolean use_ssl = false;
+
+  /** Trust store path for TLS connections. */
   private String trust_store;
+
+  /** Trust store password for TLS connections. */
   private String trust_store_pwd;
-  private Integer connection_timeout_in_ms;
-  private ZoneId zone_id;
+
+  /** Connection timeout in milliseconds; zero means no timeout. */
+  private Integer connection_timeout_in_ms = SessionConfig.DEFAULT_CONNECTION_TIMEOUT_MS;
+
+  /** Session timezone; defaults to the JVM timezone. */
+  private ZoneId zone_id = ZoneId.systemDefault();
+
+  /** Initial Thrift buffer size in bytes. */
   private Integer thrift_default_buffer_size = 1024;
+
+  /** Maximum Thrift frame size in bytes. */
   private Integer thrift_max_frame_size = 67108864;
+
+  /** Redirect writes to the relevant leader; the starter retains its historical false default. */
   private boolean enable_redirection;
+
+  /** Automatically convert records to tablets in the tree pool (true in IoTDB 2.0.11). */
   private boolean enable_records_auto_convert_tablet =
       SessionConfig.DEFAULT_RECORDS_AUTO_CONVERT_TABLET;
 
@@ -81,10 +125,21 @@ public class IoTDBSessionProperties {
     this.database = database;
   }
 
+  /**
+   * @deprecated Both pool types are exposed; select the dialect by injecting the required type.
+   */
+  @Deprecated
+  @DeprecatedConfigurationProperty(
+      reason =
+          "Both pool types are exposed; select the dialect by injecting the required pool type.")
   public String getSql_dialect() {
     return sql_dialect;
   }
 
+  /**
+   * @deprecated Retained for configuration compatibility; this does not select a pool type.
+   */
+  @Deprecated
   public void setSql_dialect(String sql_dialect) {
     this.sql_dialect = sql_dialect;
   }
diff --git a/iotdb-spring-boot-starter/src/main/java/org/apache/iotdb/session/IoTDBSessionPool.java b/iotdb-spring-boot-starter/src/main/java/org/apache/iotdb/session/IoTDBSessionPool.java
index 4a56429..2d741e9 100644
--- a/iotdb-spring-boot-starter/src/main/java/org/apache/iotdb/session/IoTDBSessionPool.java
+++ b/iotdb-spring-boot-starter/src/main/java/org/apache/iotdb/session/IoTDBSessionPool.java
@@ -23,91 +23,92 @@ import org.apache.iotdb.isession.pool.ITableSessionPool;
 import org.apache.iotdb.session.pool.SessionPool;
 import org.apache.iotdb.session.pool.TableSessionPoolBuilder;
 
+import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
 
 import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
 
-@Configuration
-@ConditionalOnClass({IoTDBSessionProperties.class})
+@AutoConfiguration
+@ConditionalOnClass({SessionPool.class, TableSessionPoolBuilder.class})
 @EnableConfigurationProperties(IoTDBSessionProperties.class)
 public class IoTDBSessionPool {
 
   private final IoTDBSessionProperties properties;
-  private ITableSessionPool tableSessionPool;
-  private ISessionPool treeSessionPool;
 
   public IoTDBSessionPool(IoTDBSessionProperties properties) {
     this.properties = properties;
   }
 
-  @Bean
+  @Bean(destroyMethod = "close")
+  @ConditionalOnMissingBean(ITableSessionPool.class)
   public ITableSessionPool tableSessionPool() {
-    if (tableSessionPool == null) {
-      synchronized (IoTDBSessionPool.class) {
-        if (tableSessionPool == null) {
-          tableSessionPool =
-              new TableSessionPoolBuilder()
-                  .nodeUrls(Arrays.asList(properties.getNode_urls().split(";")))
-                  .user(properties.getUsername())
-                  .password(properties.getPassword())
-                  .database(properties.getDatabase())
-                  .maxSize(properties.getMax_size())
-                  .fetchSize(properties.getFetch_size())
-                  .enableAutoFetch(properties.isEnable_auto_fetch())
-                  .useSSL(properties.isUse_ssl())
-                  .queryTimeoutInMs(properties.getQuery_timeout_in_ms())
-                  .maxRetryCount(properties.getMax_retry_count())
-                  .waitToGetSessionTimeoutInMs(properties.getWait_to_get_session_timeout_in_ms())
-                  .enableCompression(properties.isEnable_compression())
-                  .retryIntervalInMs(properties.getRetry_interval_in_ms())
-                  .trustStore(properties.getTrust_store())
-                  .trustStorePwd(properties.getTrust_store_pwd())
-                  .connectionTimeoutInMs(properties.getConnection_timeout_in_ms())
-                  .zoneId(properties.getZone_id())
-                  .thriftDefaultBufferSize(properties.getThrift_default_buffer_size())
-                  .thriftMaxFrameSize(properties.getThrift_max_frame_size())
-                  .enableRedirection(properties.isEnable_redirection())
-                  .build();
-        }
-      }
-    }
-    return tableSessionPool;
+    return new TableSessionPoolBuilder()
+        .nodeUrls(nodeUrls())
+        .user(properties.getUsername())
+        .password(properties.getPassword())
+        .database(properties.getDatabase())
+        .maxSize(properties.getMax_size())
+        .fetchSize(properties.getFetch_size())
+        .enableAutoFetch(properties.isEnable_auto_fetch())
+        .useSSL(properties.isUse_ssl())
+        .queryTimeoutInMs(properties.getQuery_timeout_in_ms())
+        .maxRetryCount(properties.getMax_retry_count())
+        .waitToGetSessionTimeoutInMs(properties.getWait_to_get_session_timeout_in_ms())
+        .enableThriftCompression(properties.isEnable_compression())
+        .retryIntervalInMs(properties.getRetry_interval_in_ms())
+        .trustStore(properties.getTrust_store())
+        .trustStorePwd(properties.getTrust_store_pwd())
+        .connectionTimeoutInMs(properties.getConnection_timeout_in_ms())
+        .zoneId(properties.getZone_id())
+        .thriftDefaultBufferSize(properties.getThrift_default_buffer_size())
+        .thriftMaxFrameSize(properties.getThrift_max_frame_size())
+        .enableRedirection(properties.isEnable_redirection())
+        .build();
   }
 
-  @Bean
+  @Bean(destroyMethod = "close")
+  @ConditionalOnMissingBean(ISessionPool.class)
   public ISessionPool treeSessionPool() {
-    if (treeSessionPool == null) {
-      synchronized (IoTDBSessionPool.class) {
-        if (treeSessionPool == null) {
-          treeSessionPool =
-              new SessionPool.Builder()
-                  .nodeUrls(Arrays.asList(properties.getNode_urls().split(";")))
-                  .user(properties.getUsername())
-                  .password(properties.getPassword())
-                  .maxSize(properties.getMax_size())
-                  .fetchSize(properties.getFetch_size())
-                  .enableAutoFetch(properties.isEnable_auto_fetch())
-                  .useSSL(properties.isUse_ssl())
-                  .queryTimeoutInMs(properties.getQuery_timeout_in_ms())
-                  .maxRetryCount(properties.getMax_retry_count())
-                  .waitToGetSessionTimeoutInMs(properties.getWait_to_get_session_timeout_in_ms())
-                  .enableCompression(properties.isEnable_compression())
-                  .retryIntervalInMs(properties.getRetry_interval_in_ms())
-                  .trustStore(properties.getTrust_store())
-                  .trustStorePwd(properties.getTrust_store_pwd())
-                  .connectionTimeoutInMs(properties.getConnection_timeout_in_ms())
-                  .zoneId(properties.getZone_id())
-                  .thriftDefaultBufferSize(properties.getThrift_default_buffer_size())
-                  .thriftMaxFrameSize(properties.getThrift_max_frame_size())
-                  .enableRedirection(properties.isEnable_redirection())
-                  .enableRecordsAutoConvertTablet(properties.isEnable_records_auto_convert_tablet())
-                  .build();
-        }
-      }
+    return new SessionPool.Builder()
+        .nodeUrls(nodeUrls())
+        .user(properties.getUsername())
+        .password(properties.getPassword())
+        .maxSize(properties.getMax_size())
+        .fetchSize(properties.getFetch_size())
+        .enableAutoFetch(properties.isEnable_auto_fetch())
+        .useSSL(properties.isUse_ssl())
+        .queryTimeoutInMs(properties.getQuery_timeout_in_ms())
+        .maxRetryCount(properties.getMax_retry_count())
+        .waitToGetSessionTimeoutInMs(properties.getWait_to_get_session_timeout_in_ms())
+        .enableThriftRpcCompaction(properties.isEnable_compression())
+        .retryIntervalInMs(properties.getRetry_interval_in_ms())
+        .trustStore(properties.getTrust_store())
+        .trustStorePwd(properties.getTrust_store_pwd())
+        .connectionTimeoutInMs(properties.getConnection_timeout_in_ms())
+        .zoneId(properties.getZone_id())
+        .thriftDefaultBufferSize(properties.getThrift_default_buffer_size())
+        .thriftMaxFrameSize(properties.getThrift_max_frame_size())
+        .enableRedirection(properties.isEnable_redirection())
+        .enableRecordsAutoConvertTablet(properties.isEnable_records_auto_convert_tablet())
+        .build();
+  }
+
+  private List<String> nodeUrls() {
+    String configuredUrls = properties.getNode_urls();
+    if (configuredUrls == null || configuredUrls.trim().isEmpty()) {
+      throw new IllegalArgumentException(
+          "iotdb.session.node-urls must contain at least one host:port");
+    }
+    List<String> urls =
+        Arrays.stream(configuredUrls.split(";", -1)).map(String::trim).collect(Collectors.toList());
+    if (urls.stream().anyMatch(String::isEmpty)) {
+      throw new IllegalArgumentException("iotdb.session.node-urls contains an empty endpoint");
     }
-    return treeSessionPool;
+    return urls;
   }
 }
diff --git a/iotdb-spring-boot-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/iotdb-spring-boot-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json
new file mode 100644
index 0000000..7398571
--- /dev/null
+++ b/iotdb-spring-boot-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -0,0 +1,20 @@
+{
+  "properties": [
+    {
+      "name": "iotdb.session.fetch-size",
+      "defaultValue": 5000
+    },
+    {
+      "name": "iotdb.session.connection-timeout-in-ms",
+      "defaultValue": 0
+    },
+    {
+      "name": "iotdb.session.retry-interval-in-ms",
+      "defaultValue": 500
+    },
+    {
+      "name": "iotdb.session.enable-records-auto-convert-tablet",
+      "defaultValue": true
+    }
+  ]
+}
diff --git a/iotdb-spring-boot-starter/src/test/java/org/apache/iotdb/session/IoTDBSessionPoolTest.java b/iotdb-spring-boot-starter/src/test/java/org/apache/iotdb/session/IoTDBSessionPoolTest.java
new file mode 100644
index 0000000..8e7804c
--- /dev/null
+++ b/iotdb-spring-boot-starter/src/test/java/org/apache/iotdb/session/IoTDBSessionPoolTest.java
@@ -0,0 +1,348 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.iotdb.session;
+
+import org.apache.iotdb.config.IoTDBSessionProperties;
+import org.apache.iotdb.isession.SessionConfig;
+import org.apache.iotdb.isession.pool.ISessionPool;
+import org.apache.iotdb.isession.pool.ITableSessionPool;
+import org.apache.iotdb.session.pool.SessionPool;
+
+import org.junit.Test;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Configuration;
+
+import java.lang.reflect.Proxy;
+import java.time.ZoneId;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.UnaryOperator;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class IoTDBSessionPoolTest {
+  private final ApplicationContextRunner contextRunner =
+      new ApplicationContextRunner()
+          .withConfiguration(AutoConfigurations.of(IoTDBSessionPool.class))
+          .withPropertyValues("iotdb.session.enable-auto-fetch=false");
+
+  private final ApplicationContextRunner autoDiscoveryContextRunner =
+      new ApplicationContextRunner()
+          .withUserConfiguration(AutoConfigurationApplication.class)
+          .withPropertyValues("iotdb.session.enable-auto-fetch=false");
+
+  @Test
+  public void createsBothPoolsWithDefaults() {
+    contextRunner.run(
+        context -> {
+          assertThat(context)
+              .hasNotFailed()
+              .hasSingleBean(ISessionPool.class)
+              .hasSingleBean(ITableSessionPool.class);
+          IoTDBSessionProperties properties = context.getBean(IoTDBSessionProperties.class);
+          assertThat(properties.getConnection_timeout_in_ms()).isZero();
+          assertThat(properties.getFetch_size()).isEqualTo(SessionConfig.DEFAULT_FETCH_SIZE);
+          assertDefaultPoolParameters(context.getBean(ISessionPool.class));
+          assertThat(context.getBean(ITableSessionPool.class))
+              .extracting("sessionPool")
+              .isInstanceOfSatisfying(SessionPool.class, this::assertDefaultPoolParameters);
+        });
+  }
+
+  private void assertDefaultPoolParameters(ISessionPool pool) {
+    assertThat(pool.getFetchSize()).isEqualTo(SessionConfig.DEFAULT_FETCH_SIZE);
+    assertThat(pool)
+        .extracting(
+            "connectionTimeoutInMs",
+            "retryIntervalInMs",
+            "enableRecordsAutoConvertTablet",
+            "enableRedirection",
+            "enableThriftCompression",
+            "enableIoTDBRpcCompression")
+        .containsExactly(0, 500L, true, false, false, true);
+  }
+
+  @Test
+  public void discoversAutoConfigurationFromImportsFile() {
+    autoDiscoveryContextRunner.run(
+        context ->
+            assertThat(context)
+                .hasNotFailed()
+                .hasSingleBean(IoTDBSessionPool.class)
+                .hasSingleBean(ISessionPool.class)
+                .hasSingleBean(ITableSessionPool.class));
+  }
+
+  @Test
+  public void bindsLegacyAndCanonicalPropertyNames() {
+    contextRunner
+        .withPropertyValues(
+            "iotdb.session.node_urls=localhost:6667",
+            "iotdb.session.connection-timeout-in-ms=2500",
+            "iotdb.session.max_size=3")
+        .run(
+            context -> {
+              assertThat(context).hasNotFailed();
+              IoTDBSessionProperties properties = context.getBean(IoTDBSessionProperties.class);
+              assertThat(properties.getNode_urls()).isEqualTo("localhost:6667");
+              assertThat(properties.getConnection_timeout_in_ms()).isEqualTo(2500);
+              assertThat(properties.getMax_size()).isEqualTo(3);
+            });
+  }
+
+  @Test
+  public void appliesAllCanonicalParametersToBothPools() {
+    assertParameterBinding(UnaryOperator.identity());
+  }
+
+  @Test
+  public void appliesAllUnderscoreParametersToBothPools() {
+    assertParameterBinding(name -> name.replace('-', '_'));
+  }
+
+  @Test
+  public void appliesAllCamelCaseParametersToBothPools() {
+    assertParameterBinding(
+        name -> {
+          StringBuilder result = new StringBuilder();
+          boolean capitalize = false;
+          for (char character : name.toCharArray()) {
+            if (character == '-') {
+              capitalize = true;
+            } else {
+              result.append(capitalize ? Character.toUpperCase(character) : character);
+              capitalize = false;
+            }
+          }
+          return result.toString();
+        });
+  }
+
+  private void assertParameterBinding(UnaryOperator<String> propertyName) {
+    String[] settings = {
+      "node-urls= localhost:16667 ; localhost:16668 ",
+      "username=test-user",
+      "password=test-password",
+      "database=test_database",
+      "sql-dialect=tree",
+      "max-size=3",
+      "fetch-size=2048",
+      "query-timeout-in-ms=12000",
+      "enable-auto-fetch=false",
+      "max-retry-count=4",
+      "wait-to-get-session-timeout-in-ms=2300",
+      "enable-compression=true",
+      "retry-interval-in-ms=200",
+      "use-ssl=true",
+      "trust-store=test-trust-store.jks",
+      "trust-store-pwd=test-trust-store-password",
+      "connection-timeout-in-ms=2500",
+      "zone-id=Asia/Shanghai",
+      "thrift-default-buffer-size=4096",
+      "thrift-max-frame-size=33554432",
+      "enable-redirection=true",
+      "enable-records-auto-convert-tablet=false"
+    };
+    String[] configuredProperties =
+        Arrays.stream(settings)
+            .map(
+                setting -> {
+                  int separator = setting.indexOf('=');
+                  return "iotdb.session."
+                      + propertyName.apply(setting.substring(0, separator))
+                      + setting.substring(separator);
+                })
+            .toArray(String[]::new);
+    new ApplicationContextRunner()
+        .withConfiguration(AutoConfigurations.of(IoTDBSessionPool.class))
+        .withPropertyValues(configuredProperties)
+        .run(
+            context -> {
+              assertThat(context).hasNotFailed();
+              assertThat(context.getBean(IoTDBSessionProperties.class).getSql_dialect())
+                  .isEqualTo("tree");
+              assertConfiguredPoolParameters(
+                  (SessionPool) context.getBean(ISessionPool.class), false);
+              assertThat(context.getBean(ITableSessionPool.class))
+                  .extracting("sessionPool")
+                  .isInstanceOfSatisfying(
+                      SessionPool.class, pool -> assertConfiguredPoolParameters(pool, true));
+            });
+  }
+
+  private void assertConfiguredPoolParameters(SessionPool pool, boolean table) {
+    assertThat(pool.getUser()).isEqualTo("test-user");
+    assertThat(pool.getPassword()).isEqualTo("test-password");
+    assertThat(pool.getMaxSize()).isEqualTo(3);
+    assertThat(pool.getFetchSize()).isEqualTo(2048);
+    assertThat(pool.getQueryTimeout()).isEqualTo(12000L);
+    assertThat(pool.getWaitToGetSessionTimeoutInMs()).isEqualTo(2300L);
+    assertThat(pool.getConnectionTimeoutInMs()).isEqualTo(2500);
+    assertThat(pool.getZoneId()).isEqualTo(ZoneId.of("Asia/Shanghai"));
+    assertThat(pool.isEnableThriftCompression()).isTrue();
+    assertThat(pool.isEnableRedirection()).isTrue();
+    // TableSessionPool has no configuration getters; inspect its underlying pool without opening
+    // a connection. The remaining SessionPool settings also have no public getters.
+    assertThat(pool)
+        .extracting(
+            "nodeUrls",
+            "database",
+            "sqlDialect",
+            "enableAutoFetch",
+            "maxRetryCount",
+            "retryIntervalInMs",
+            "useSSL",
+            "trustStore",
+            "trustStorePwd",
+            "thriftDefaultBufferSize",
+            "thriftMaxFrameSize",
+            "enableRecordsAutoConvertTablet",
+            "enableIoTDBRpcCompression")
+        .containsExactly(
+            List.of("localhost:16667", "localhost:16668"),
+            table ? "test_database" : null,
+            table ? "table" : "tree",
+            false,
+            4,
+            200L,
+            true,
+            "test-trust-store.jks",
+            "test-trust-store-password",
+            4096,
+            33554432,
+            table,
+            true);
+  }
+
+  @Test
+  public void preservesSpecialTimeoutAndRetryValues() {
+    for (long timeout : new long[] {-1, 0}) {
+      contextRunner
+          .withPropertyValues(
+              "iotdb.session.query-timeout-in-ms=" + timeout,
+              "iotdb.session.max-retry-count=0",
+              "iotdb.session.retry-interval-in-ms=0")
+          .run(
+              context -> {
+                assertThat(context).hasNotFailed();
+                assertThat(context.getBean(ISessionPool.class))
+                    .extracting("queryTimeoutInMs", "maxRetryCount", "retryIntervalInMs")
+                    .containsExactly(timeout, 0, 0L);
+                assertThat(context.getBean(ITableSessionPool.class))
+                    .extracting("sessionPool")
+                    .extracting("queryTimeoutInMs", "maxRetryCount", "retryIntervalInMs")
+                    .containsExactly(timeout, 0, 0L);
+              });
+    }
+  }
+
+  @Test
+  public void acceptsWhitespaceAroundEndpoints() {
+    contextRunner
+        .withPropertyValues("iotdb.session.node-urls= localhost:6667 ; localhost:6668 ")
+        .run(context -> assertThat(context).hasNotFailed());
+  }
+
+  @Test
+  public void rejectsAnEmptyEndpointWithAConfigurationError() {
+    contextRunner
+        .withPropertyValues("iotdb.session.node-urls=localhost:6667;")
+        .run(
+            context ->
+                assertThat(context)
+                    .hasFailed()
+                    .getFailure()
+                    .hasRootCauseMessage("iotdb.session.node-urls contains an empty endpoint"));
+  }
+
+  @Test
+  public void backsOffForUserPoolsAndClosesThemWithTheContext() {
+    AtomicInteger closes = new AtomicInteger();
+    ISessionPool tree = pool(ISessionPool.class, closes);
+    ITableSessionPool table = pool(ITableSessionPool.class, closes);
+    contextRunner
+        .withBean(
+            "customTreePool",
+            ISessionPool.class,
+            () -> tree,
+            definition -> definition.setDestroyMethodName("close"))
+        .withBean(
+            "customTablePool",
+            ITableSessionPool.class,
+            () -> table,
+            definition -> definition.setDestroyMethodName("close"))
+        .run(
+            context -> {
+              assertThat(context)
+                  .hasNotFailed()
+                  .hasSingleBean(ISessionPool.class)
+                  .hasSingleBean(ITableSessionPool.class);
+              assertThat(context.getBean(ISessionPool.class)).isSameAs(tree);
+              assertThat(context.getBean(ITableSessionPool.class)).isSameAs(table);
+            });
+    assertThat(closes).hasValue(2);
+  }
+
+  @Test
+  public void closesAutoConfiguredPoolsOnContextShutdown() {
+    AtomicReference<ISessionPool> tree = new AtomicReference<>();
+    AtomicReference<ITableSessionPool> table = new AtomicReference<>();
+    contextRunner.run(
+        context -> {
+          tree.set(context.getBean(ISessionPool.class));
+          table.set(context.getBean(ITableSessionPool.class));
+        });
+    assertThatThrownBy(() -> tree.get().executeQueryStatement("show databases"))
+        .hasMessageContaining("closed");
+    assertThatThrownBy(() -> table.get().getSession()).hasMessageContaining("closed");
+  }
+
+  @Test
+  public void customTreePoolDoesNotDisableTheTablePool() {
+    ISessionPool tree = pool(ISessionPool.class, new AtomicInteger());
+    contextRunner
+        .withBean(ISessionPool.class, () -> tree)
+        .run(
+            context -> {
+              assertThat(context).hasNotFailed().hasSingleBean(ITableSessionPool.class);
+              assertThat(context.getBean(ISessionPool.class)).isSameAs(tree);
+            });
+  }
+
+  private static <T> T pool(Class<T> type, AtomicInteger closes) {
+    return type.cast(
+        Proxy.newProxyInstance(
+            type.getClassLoader(),
+            new Class<?>[] {type},
+            (proxy, method, args) -> {
+              if (method.getName().equals("close")) {
+                closes.incrementAndGet();
+              }
+              return null;
+            }));
+  }
+
+  @Configuration(proxyBeanMethods = false)
+  @EnableAutoConfiguration
+  static class AutoConfigurationApplication {}
+}
diff --git a/iotdb-thingsboard-table/CI-NOTES.md b/iotdb-thingsboard-table/CI-NOTES.md
index f890466..050f1ae 100644
--- a/iotdb-thingsboard-table/CI-NOTES.md
+++ b/iotdb-thingsboard-table/CI-NOTES.md
@@ -25,19 +25,15 @@ The `iotdb-extras` parent reactor builds and tests this module through the named
 `with-thingsboard` profile (the module compiles with Java 17 language features and
 integrates ThingsBoard SPIs, so it is an explicit opt-in rather than part of the
 default reactor). CI activates it on the JDK 17+ jobs by passing `-P with-thingsboard`
-(see `.github/workflows/compile-check.yml`); the JDK 8/11 jobs omit the flag and skip
-it. The module overrides NO shared reactor versions — iotdb-session, tsfile and guava
-are inherited from the parent (2.0.5 / 2.1.1 / 32.1.2-jre), like the sibling IoTDB
-connectors; the only deliberate override is jakarta.validation-api 3.0.2 (the
-ThingsBoard 4.3.x Spring Boot 3 runtime namespace). tsfile resolves to the reactor's
-single 2.1.1, so this module introduces no tsfile-convergence conflict. Note: running
-`mvn -P enforce` still reports one pre-existing `dependencyConvergence` finding on
-`org.apache.httpcomponents:httpcore` (4.4.12 vs 4.4.16), which comes transitively from
-`iotdb-session 2.0.5 -> libthrift 0.14.1` and is shared by every iotdb-session consumer
-in the reactor (the parent pom pins httpclient but not httpcore); it is not introduced
-by this module and would be resolved at the parent level. This file is a developer
-reference of the local checks for the `iotdb-thingsboard-table` module; it is not itself
-a GitHub Actions workflow.
+(see `.github/workflows/compile-check.yml`). CI uses JDK 17 and 21. IoTDB client,
+TsFile and Guava versions follow the reactor (2.0.11 / 2.4.0 / 32.1.2-jre).
+Jakarta validation remains a module-local 3.0.2 override to match the ThingsBoard
+Spring Boot 3 host. Run dependency convergence separately after dependency changes;
+a compile result does not establish convergence or real-host binary compatibility.
+
+This file documents local checks; it is not a GitHub Actions workflow. Container
+tests default to `apache/iotdb:2.0.11-standalone`; `-Diotdb.test.image` can select
+another released server for compatibility testing.
 
 ## Candidate Checks
 
diff --git a/iotdb-thingsboard-table/README.md b/iotdb-thingsboard-table/README.md
index 9e0b811..827f560 100644
--- a/iotdb-thingsboard-table/README.md
+++ b/iotdb-thingsboard-table/README.md
@@ -26,15 +26,17 @@
 `iotdb-thingsboard-table` is a ThingsBoard historical-telemetry DAO backend
 built on Apache IoTDB Table Mode. It lets a ThingsBoard deployment store
 and serve time-series telemetry through IoTDB's table-session API instead of the
-default Cassandra/SQL backends. It compiles against the reactor's IoTDB 2.0.5
-table-session client; its integration tests run the real write, read,
+default Cassandra/SQL backends. It compiles against the reactor's IoTDB 2.0.11
+table-session client; its integration tests are configured to run the real write, read,
 aggregation, latest-telemetry, attribute and retention paths against an
-`apache/iotdb:2.0.8-standalone` server. The module targets ThingsBoard v4.3.1.2. Because
+`apache/iotdb:2.0.11-standalone` server. The module targets ThingsBoard v4.3.1.2. Because
 it compiles with Java 17 language features (records and others), the
 `iotdb-extras` parent reactor builds and tests it only on JDK 17+ through the
 explicit, named `with-thingsboard` opt-in profile (it is not JDK-auto-activated):
-CI passes `-P with-thingsboard` on the JDK 17/21 jobs while the 8/11 jobs omit it
-and skip the module, and a plain reactor build never pulls it in.
+CI passes `-P with-thingsboard` on both JDK 17 and JDK 21 jobs.
+A plain reactor build never pulls it in.
+
+Container tests accept `-Diotdb.test.image=apache/iotdb:<version>-standalone` for additional compatibility checks.
 
 ## ThingsBoard SPI surface (Strategy F)
 
@@ -234,7 +236,7 @@ a retention window in **milliseconds**, and the accepted forms are narrow:
 | `TTL='INF'` | Never expire. The **quoted** string is the only accepted spelling; this is the form `entity_attributes` and `telemetry_latest` ship with. |
 | `TTL=DEFAULT` | Inherit the database default, which is `INF` on a fresh node. |
 
-Anything else is rejected by IoTDB 2.0.8: an unquoted `TTL=INF` is parsed as an
+Anything else is rejected by IoTDB 2.0.11 (and 2.0.8): an unquoted `TTL=INF` is parsed as an
 identifier (`ttl value must be a LongLiteral, but now is Identifier`), and any
 other quoted value — including a quoted number (`'604800000'`) or a duration
 (`'7d'`) — fails with `ttl value must be 'INF' or a long literal`.
@@ -265,7 +267,8 @@ SELECT table_name, "ttl(ms)" FROM information_schema.tables WHERE database='thin
 
 Either way a never-expiring table reads back as `INF` and a concrete retention as
 the millisecond number.
-`IoTDBTableTtlIT` pins both paths against a real IoTDB 2.0.8 container. It
+When the `iotdb-table-it` profile runs, `IoTDBTableTtlIT` exercises both paths against a real
+IoTDB 2.0.11 container. It
 verifies the TTL **property mechanism** only and deliberately does not assert
 physical row eviction, because eviction is asynchronous and compaction-driven and
 so is not deterministic inside a test.
diff --git a/iotdb-thingsboard-table/docker-compose.bench.yml b/iotdb-thingsboard-table/docker-compose.bench.yml
index 016d7c1..ba5bb74 100644
--- a/iotdb-thingsboard-table/docker-compose.bench.yml
+++ b/iotdb-thingsboard-table/docker-compose.bench.yml
@@ -19,7 +19,7 @@
 
 # TC-1 ingestion-throughput SMOKE-profile bench stack.
 #
-# This is the smoke profile: a single, clean-volume IoTDB 2.0.8 node for a fast,
+# This is the smoke profile: a single, clean-volume IoTDB 2.0.11 node for a fast,
 # reproducible local benchmark run. It mirrors docker-compose.test.yml but ships
 # only the IoTDB service on a dedicated fresh volume so each run starts from an
 # empty store. The benchmark IT (IoTDBTableIngestionBenchmarkIT) provisions its
@@ -34,7 +34,7 @@
 
 services:
   iotdb:
-    image: apache/iotdb:2.0.8-standalone
+    image: apache/iotdb:2.0.11-standalone
     container_name: iotdb-table-bench
     environment:
       IOTDB_USERNAME: ${IOTDB_USERNAME:?set IOTDB_USERNAME}
diff --git a/iotdb-thingsboard-table/docker-compose.test.yml b/iotdb-thingsboard-table/docker-compose.test.yml
index f001484..0359a63 100644
--- a/iotdb-thingsboard-table/docker-compose.test.yml
+++ b/iotdb-thingsboard-table/docker-compose.test.yml
@@ -19,7 +19,7 @@
 
 services:
   iotdb:
-    image: apache/iotdb:2.0.8-standalone
+    image: apache/iotdb:2.0.11-standalone
     container_name: iotdb-table-test
     environment:
       IOTDB_USERNAME: ${IOTDB_USERNAME:?set IOTDB_USERNAME}
diff --git a/iotdb-thingsboard-table/docs/benchmarks/README.md b/iotdb-thingsboard-table/docs/benchmarks/README.md
index 898c0a8..50c70e2 100644
--- a/iotdb-thingsboard-table/docs/benchmarks/README.md
+++ b/iotdb-thingsboard-table/docs/benchmarks/README.md
@@ -59,7 +59,7 @@ dao.save(tenant, entity, tsKvEntry, ttl)
   -> writer.enqueue(...)           bounded ArrayBlockingQueue (capacity 50,000)
   -> single flush worker           batches up to 500 rows, maxLingerMs 20
   -> Tablet insert                 multi-row table-session insert
-  -> real IoTDB 2.0.8              apache/iotdb:2.0.8-standalone Testcontainer
+  -> real IoTDB 2.0.11             apache/iotdb:2.0.11-standalone Testcontainer
 ```
 
 It runs `SAVER_THREADS = 50` concurrent threads, each writing
@@ -148,7 +148,7 @@ lack of Docker.
 
 ### Smoke stack
 
-The benchmark IT manages its own throwaway `apache/iotdb:2.0.8-standalone`
+The benchmark IT manages its own throwaway `apache/iotdb:2.0.11-standalone`
 Testcontainer, so no external stack is required to run it. For a manual run
 against a standalone node instead of the throwaway container, the module's
 [`../../docker-compose.test.yml`](../../docker-compose.test.yml) brings up an
diff --git a/iotdb-thingsboard-table/docs/migration-guide.md b/iotdb-thingsboard-table/docs/migration-guide.md
index a16b755..3fbba63 100644
--- a/iotdb-thingsboard-table/docs/migration-guide.md
+++ b/iotdb-thingsboard-table/docs/migration-guide.md
@@ -67,20 +67,19 @@ TsFile versions are inherited from the `iotdb-extras` parent reactor.
 | --- | --- | --- | --- |
 | Module | `org.apache.iotdb:iotdb-thingsboard-table` | `2.0.4-SNAPSHOT` (parent version) | module `pom.xml` `<parent>` |
 | Parent reactor | `org.apache.iotdb:iotdb-extras-parent` | `2.0.4-SNAPSHOT` | module `pom.xml` `<parent>` |
-| IoTDB session client | `org.apache.iotdb:iotdb-session` | `2.0.5` | inherited from parent `iotdb.version`; module declares the dependency with no `<version>` |
-| TsFile | `org.apache.tsfile:tsfile` | `2.1.1` | inherited from parent `tsfile.version` (transitive of `iotdb-session`) |
+| IoTDB session client | `org.apache.iotdb:iotdb-session` | `2.0.11` | inherited from parent `iotdb.version`; module declares the dependency with no `<version>` |
+| TsFile | `org.apache.tsfile:tsfile` | `2.4.0` | inherited from parent `tsfile.version` (transitive of `iotdb-session`) |
 | Guava | `com.google.guava:guava` | `32.1.2-jre` | inherited from parent `guava.version`; `provided` scope |
 | Bean-validation API | `jakarta.validation:jakarta.validation-api` | `3.0.2` | module override (`jakarta.* ` namespace, `provided` scope) to match the Spring Boot 3 runtime host |
 | ThingsBoard host | (ThingsBoard distribution) | `4.3.1.2` | module `pom.xml` `thingsboard.version`; SPI surface verified against this tag |
-| IoTDB server (tested) | `apache/iotdb` standalone image | `2.0.8` | integration tests run against `apache/iotdb:2.0.8-standalone` |
+| IoTDB server (test default) | `apache/iotdb` standalone image | `2.0.11` | integration tests select `apache/iotdb:2.0.11-standalone` |
 
 Notes:
 
-- The module compiles against the **2.0.5** table-session client; its
-  integration tests exercise the real write path against an **IoTDB 2.0.8**
-  standalone server, so the 2.0.5-client / 2.0.8-server RPC path is the
-  validated pairing. Any IoTDB 2.x server that speaks the same table-session RPC
-  is a candidate, but 2.0.8 is the version the module is tested against.
+- The module compiles against the **2.0.11** table-session client. Integration
+  tests default to an **IoTDB 2.0.11** standalone server and allow overriding
+  `iotdb.test.image`. Run the profile to validate your actual client/server pairing;
+  a configured version is not evidence of successful container execution.
 - The build parent itself targets an older Spring line; the module deploys into
   ThingsBoard 4.3.x, which runs Spring Boot 3.5.x / Spring 6 / JDK 17 (the
   `jakarta.*` namespace). That is why `jakarta.validation-api` is overridden to
@@ -93,7 +92,7 @@ Notes:
 ## Before you begin
 
 - A reachable IoTDB 2.x server in **Table Mode** (the tested server is IoTDB
-  2.0.8). Have its host, port, and credentials ready.
+  2.0.11). Have its host, port, and credentials ready.
 - A ThingsBoard 4.3.1.2 build you control (the module is consumed as a
   compile/runtime dependency of the ThingsBoard application; it is not a
   drop-in for a pre-built binary distribution).
@@ -228,7 +227,7 @@ ALTER TABLE telemetry SET PROPERTIES TTL=DEFAULT;      -- back to the db default
 ```
 
 Quoted numbers (`'604800000'`) and duration forms (`'7d'`) are rejected by IoTDB
-2.0.8. The `telemetry_latest` overlay table is created `WITH (TTL='INF')` and is
+2.0.11 (and 2.0.8). The `telemetry_latest` overlay table is created `WITH (TTL='INF')` and is
 **exempt** from the `telemetry` TTL: setting a retention window on `telemetry`
 does not evict overlay rows, so bound the overlay separately if needed (see
 *Limitations*).
diff --git a/iotdb-thingsboard-table/docs/user-guide.md b/iotdb-thingsboard-table/docs/user-guide.md
index b96e57e..df84f8c 100644
--- a/iotdb-thingsboard-table/docs/user-guide.md
+++ b/iotdb-thingsboard-table/docs/user-guide.md
@@ -36,9 +36,9 @@ Cassandra / SQL backends. It implements ThingsBoard's `TimeseriesDao`,
 auto-configuration, so it activates inside a real ThingsBoard deployment without
 the host application having to component-scan the module's package.
 
-The module is built against the reactor's IoTDB 2.0.5 table-session client and
+The module is built against the reactor's IoTDB 2.0.11 table-session client and
 its integration tests exercise the real write path against an
-`apache/iotdb:2.0.8-standalone` server. It targets ThingsBoard `4.3.1.2` and
+`apache/iotdb:2.0.11-standalone` server. It targets ThingsBoard `4.3.1.2` and
 requires JDK 17+.
 
 **It is default-inert.** None of the three DAOs activate unless the operator
diff --git a/iotdb-thingsboard-table/pom.xml b/iotdb-thingsboard-table/pom.xml
index 63e8f7b..6e1f39d 100644
--- a/iotdb-thingsboard-table/pom.xml
+++ b/iotdb-thingsboard-table/pom.xml
@@ -36,15 +36,9 @@
         <maven.compiler.release>17</maven.compiler.release>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <!--
-            Version policy: this module overrides NO shared reactor versions. iotdb-session, tsfile
-            and guava are inherited from the parent reactor (2.0.5 / 2.1.1 / 32.1.2-jre), exactly like
-            the sibling spark / flink IoTDB connectors. The Table-Mode client API this module uses
-            (TableSessionPoolBuilder / ITableSessionPool, Tablet with ColumnCategory TAG/FIELD,
-            TSDataType, TableSessionPoolBuilder#enableCompression) is present in iotdb-session 2.0.5 /
-            tsfile 2.1.1 — the parent's native pairing — so a full-reactor build keeps a SINGLE tsfile
-            version across all connectors. The integration test exercises the real write path against
-            the apache/iotdb:2.0.8-standalone server (the 2.0.5 client / 2.0.8 server RPC path is
-            covered by the container ITs).
+            IoTDB client, TsFile and Guava versions follow the reactor's released IoTDB 2.0.11
+            baseline. Optional container tests default to apache/iotdb:2.0.11-standalone;
+            use -Diotdb.test.image to check another released server version.
 
             The one deliberate override is jakarta.validation-api: this module deploys into
             ThingsBoard 4.3.1.2, which runs on Spring Boot 3.5.14 / Spring 6 / JDK 17 (the jakarta.*
@@ -75,7 +69,7 @@
         <dependency>
             <groupId>org.apache.iotdb</groupId>
             <artifactId>iotdb-session</artifactId>
-            <!-- version inherited from the parent reactor (iotdb.version = 2.0.5) -->
+            <!-- version inherited from the parent reactor (iotdb.version = 2.0.11) -->
         </dependency>
         <dependency>
             <groupId>com.google.guava</groupId>
diff --git a/iotdb-thingsboard-table/src/main/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableConfiguration.java b/iotdb-thingsboard-table/src/main/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableConfiguration.java
index 407b539..a8ab46c 100644
--- a/iotdb-thingsboard-table/src/main/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableConfiguration.java
+++ b/iotdb-thingsboard-table/src/main/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableConfiguration.java
@@ -325,7 +325,7 @@ public class IoTDBTableConfiguration {
             .database(config.getDatabase())
             .maxSize(config.getSessionPoolSize())
             .connectionTimeoutInMs(config.getConnectionTimeoutMs())
-            .enableCompression(config.isEnableCompression())
+            .enableThriftCompression(config.isEnableCompression())
             .build();
     log.info(
         "IoTDB Table Mode session pool initialized: nodeUrl={}, database={}, poolSize={}, compression={}, defaultTtlMs(storageAccountingOnly)={}",
diff --git a/iotdb-thingsboard-table/src/main/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableTimeseriesDao.java b/iotdb-thingsboard-table/src/main/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableTimeseriesDao.java
index f7c9a27..598639c 100644
--- a/iotdb-thingsboard-table/src/main/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableTimeseriesDao.java
+++ b/iotdb-thingsboard-table/src/main/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableTimeseriesDao.java
@@ -608,22 +608,21 @@ public class IoTDBTableTimeseriesDao extends IoTDBTableBaseDao
   private static String aggregationProjection(Aggregation aggregation) {
     return switch (aggregation) {
       case AVG -> "AVG(" + NUMERIC_VALUE + ") AS " + AGG_NUM_COLUMN;
-        // SUM keeps the ThingsBoard 4.3.1.2 result type: long-only buckets stay LONG, mixed buckets
-        // promote to DOUBLE. Project the partial long/double sums plus the long/double non-null
-        // counts so the row mapper can pick the type without re-reading the raw rows.
+      // SUM keeps the ThingsBoard 4.3.1.2 result type: long-only buckets stay LONG, mixed buckets
+      // promote to DOUBLE. Project the partial long/double sums plus the long/double non-null
+      // counts so the row mapper can pick the type without re-reading the raw rows.
       case SUM ->
-          // IoTDB 2.0.8 computes SUM over an INT64 column with a DOUBLE accumulator and returns a
-          // DOUBLE. Project the long partial as SUM(CAST(long_v AS DOUBLE)) -- a plain DOUBLE --
-          // and
-          // NEVER cast it back to INT64 in SQL: CAST(SUM(long_v) AS INT64) THROWS a "Double value
-          // out of range of long value" error at the IoTDB level when the long-only sum exceeds
-          // Long.MAX, which would fail the whole aggregate query before the Java
-          // bound-check/fallback
-          // could run. The DOUBLE accumulator only keeps the sum bit-exact while every partial sum
-          // stays within +/-2^53, so the row mapper reads MIN(long_v)/MAX(long_v) to bound the sum,
-          // returns the DOUBLE cast back to long (lossless within the bound) for the provably-exact
-          // long-only case, and falls back to an exact Java re-sum when the bound exceeds 2^53 (see
-          // aggregatedSumEntry). The double partial stays DOUBLE.
+          // IoTDB (verified on 2.0.8 and 2.0.11) computes SUM over an INT64 column with a DOUBLE
+          // accumulator and returns a DOUBLE. Project the long partial as
+          // SUM(CAST(long_v AS DOUBLE)) -- a plain DOUBLE -- and NEVER cast it back to INT64 in
+          // SQL: CAST(SUM(long_v) AS INT64) THROWS a "Double value out of range of long value"
+          // error at the IoTDB level when the long-only sum exceeds Long.MAX, which would fail
+          // the whole aggregate query before the Java bound-check/fallback could run. The DOUBLE
+          // accumulator only keeps the sum bit-exact while every partial sum stays within
+          // +/-2^53, so the row mapper reads MIN(long_v)/MAX(long_v) to bound the sum, returns
+          // the DOUBLE cast back to long (lossless within the bound) for the provably-exact
+          // long-only case, and falls back to an exact Java re-sum when the bound exceeds 2^53
+          // (see aggregatedSumEntry). The double partial stays DOUBLE.
           "SUM(CAST(long_v AS DOUBLE)) AS "
               + SUM_LONG_COLUMN
               + ", SUM(double_v) AS "
@@ -635,14 +634,14 @@ public class IoTDBTableTimeseriesDao extends IoTDBTableBaseDao
               + ", "
               + numericCountProjection();
       case COUNT -> countProjection();
-        // MIN/MAX keep the mixed/double numeric value via MIN/MAX(NUMERIC_VALUE) (the COALESCE
-        // promotes long_v to DOUBLE, correct for mixed and double-only buckets) and the string
-        // fallback via MIN/MAX(str_v). A long-only bucket instead reads the direct MIN(long_v)/
-        // MAX(long_v) channel, which SELECTs a stored long with no accumulation and is therefore
-        // exact for every long (even > 2^53) -- routing it through agg_num's DOUBLE would
-        // round-trip
-        // a large long and lose precision. The long/double non-null counts pick the populated
-        // channel.
+      // MIN/MAX keep the mixed/double numeric value via MIN/MAX(NUMERIC_VALUE) (the COALESCE
+      // promotes long_v to DOUBLE, correct for mixed and double-only buckets) and the string
+      // fallback via MIN/MAX(str_v). A long-only bucket instead reads the direct MIN(long_v)/
+      // MAX(long_v) channel, which SELECTs a stored long with no accumulation and is therefore
+      // exact for every long (even > 2^53) -- routing it through agg_num's DOUBLE would
+      // round-trip
+      // a large long and lose precision. The long/double non-null counts pick the populated
+      // channel.
       case MIN ->
           "MIN("
               + NUMERIC_VALUE
@@ -654,20 +653,21 @@ public class IoTDBTableTimeseriesDao extends IoTDBTableBaseDao
               + AGG_STR_COLUMN
               + ", "
               + numericCountProjection();
-        // The numeric MAX is projected as -MIN(-x) rather than MAX(x). IoTDB's GROUPED max
-        // accumulator seeds FLOAT/DOUBLE state with Float/Double.MIN_VALUE -- the smallest
-        // POSITIVE value, not the most negative one -- and only marks a group initialized when
-        // `value >= state`, so a bucket whose numeric maximum is zero or negative comes back
-        // NULL. This DAO reads a NULL aggregate as "empty bucket" and skips it, so a MAX
-        // downsampling query over e.g. a sub-zero sensor would silently lose whole buckets
-        // instead of failing. Every release up to and including 2.0.10 is affected; fixed on
-        // master by apache/iotdb#18300, which is not in a released version yet. The grouped MIN
-        // accumulator seeds with MAX_VALUE and is not affected, and IEEE-754 negation is exact,
-        // so -MIN(-x) is an exact substitute for MAX(x) over finite values, on affected and
-        // fixed servers alike. This projection is shared with the calendar path, whose
-        // non-grouped accumulators track an explicit initialized flag rather than a sentinel, so
-        // the substitution is exact there too and an empty bucket still yields NULL. MAX(long_v)
-        // and MAX(str_v) are already correct (true Long.MIN_VALUE seed / flag-based) and stay.
+      // The numeric MAX is projected as -MIN(-x) rather than MAX(x). IoTDB's GROUPED max
+      // accumulator seeds FLOAT/DOUBLE state with Float/Double.MIN_VALUE -- the smallest
+      // POSITIVE value, not the most negative one -- and only marks a group initialized when
+      // `value >= state`, so a bucket whose numeric maximum is zero or negative comes back
+      // NULL. This DAO reads a NULL aggregate as "empty bucket" and skips it, so a MAX
+      // downsampling query over e.g. a sub-zero sensor would silently lose whole buckets
+      // instead of failing. Every release up to and including 2.0.10 is affected; the fix
+      // (apache/iotdb#18300) ships in 2.0.11. The substitution is kept because the pool can be
+      // pointed at any 2.x server: the grouped MIN accumulator seeds with MAX_VALUE and is not
+      // affected, and IEEE-754 negation is exact, so -MIN(-x) is an exact substitute for MAX(x)
+      // over finite values, on affected and fixed servers alike. This projection is shared with
+      // the calendar path, whose non-grouped accumulators track an explicit initialized flag
+      // rather than a sentinel, so the substitution is exact there too and an empty bucket still
+      // yields NULL. MAX(long_v) and MAX(str_v) are already correct (true Long.MIN_VALUE seed /
+      // flag-based) and stay.
       case MAX ->
           "-1 * MIN(-1 * ("
               + NUMERIC_VALUE
diff --git a/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableAttributesDaoIT.java b/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableAttributesDaoIT.java
index 3b160fa..a0baddc 100644
--- a/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableAttributesDaoIT.java
+++ b/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableAttributesDaoIT.java
@@ -25,6 +25,7 @@ import org.apache.iotdb.session.pool.TableSessionPoolBuilder;
 
 import com.google.common.util.concurrent.ListenableFuture;
 import org.apache.commons.lang3.tuple.Pair;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.testcontainers.containers.GenericContainer;
@@ -69,6 +70,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 @Tag("integration")
 @Testcontainers(disabledWithoutDocker = true)
 class IoTDBTableAttributesDaoIT {
+  private final List<String> createdDatabases = new ArrayList<>();
   // Cold testcontainer first writes/reads are slower than a warm production node, so the
   // per-future assertion timeout is generous.
   private static final int FUTURE_TIMEOUT_SECONDS = 30;
@@ -78,7 +80,9 @@ class IoTDBTableAttributesDaoIT {
 
   @Container
   static final GenericContainer<?> IOTDB =
-      new GenericContainer<>(DockerImageName.parse("apache/iotdb:2.0.8-standalone"))
+      new GenericContainer<>(
+              DockerImageName.parse(
+                  System.getProperty("iotdb.test.image", "apache/iotdb:2.0.11-standalone")))
           .withExposedPorts(6667)
           // IoTDB binds its client RPC service to dn_rpc_address (default 127.0.0.1); bind to all
           // interfaces so the Testcontainers port-mapped session handshake succeeds.
@@ -636,12 +640,28 @@ class IoTDBTableAttributesDaoIT {
         new TestEntityId(UUID.fromString(entityId), EntityType.DEVICE));
   }
 
+  @AfterEach
+  void dropCreatedDatabases() throws Exception {
+    // Every test provisions its own database. Drop them once the test is done: the shared
+    // container has a fixed region memory budget, and leaving dozens of databases behind makes
+    // later tests in the class fail schema-region creation ("Total allocated memory for direct
+    // buffer ... is greater than limit mem cost") and time out on their first write.
+    try (ITableSessionPool pool = newPool(null);
+        ITableSession session = pool.getSession()) {
+      for (String database : createdDatabases) {
+        session.executeNonQueryStatement("DROP DATABASE IF EXISTS " + database);
+      }
+    }
+  }
+
   private String uniqueDatabase(String prefix) {
     // IoTDB caps database names at 64 chars; keep the per-test prefix short and append a trimmed
     // UUID so the total length stays well within the limit.
     String shortPrefix = prefix.length() > 12 ? prefix.substring(0, 12) : prefix;
     String shortUuid = UUID.randomUUID().toString().replace("-", "").substring(0, 16);
-    return "tb_at_" + shortPrefix + "_" + shortUuid;
+    String database = "tb_at_" + shortPrefix + "_" + shortUuid;
+    createdDatabases.add(database);
+    return database;
   }
 
   private TestAttributeKvEntry attr(long lastUpdateTs, String key, KvEntry value) {
diff --git a/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableIngestionBenchmarkIT.java b/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableIngestionBenchmarkIT.java
index 708d6b4..e0187b0 100644
--- a/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableIngestionBenchmarkIT.java
+++ b/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableIngestionBenchmarkIT.java
@@ -24,6 +24,7 @@ import org.apache.iotdb.isession.pool.ITableSessionPool;
 import org.apache.iotdb.session.pool.TableSessionPoolBuilder;
 
 import com.google.common.util.concurrent.ListenableFuture;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.slf4j.Logger;
@@ -62,7 +63,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
  * <p>TC-1 is defined with two profiles. This class is the <b>smoke profile</b>: a local, fast,
  * JUnit-driven run that exercises the real {@link IoTDBTableTimeseriesDao#save} path (bounded queue
  * &rarr; single flush worker &rarr; multi-row {@code Tablet} insert &rarr; real IoTDB) against the
- * same {@code apache/iotdb:2.0.8-standalone} Testcontainer the functional ITs use, then reports
+ * same {@code apache/iotdb:2.0.11-standalone} Testcontainer the functional ITs use, then reports
  * records/sec, error rate, and writer stats.
  *
  * <p><b>The &gt;10K writes/sec headline target is the FULL profile number on a dedicated host.</b>
@@ -79,6 +80,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 @Tag("integration")
 @Testcontainers(disabledWithoutDocker = true)
 class IoTDBTableIngestionBenchmarkIT {
+  private final List<String> createdDatabases = new ArrayList<>();
   private static final Logger LOG = LoggerFactory.getLogger(IoTDBTableIngestionBenchmarkIT.class);
 
   // Smoke sizing: concurrency mirrors the TC-1 design (50 concurrent threads) but the total row
@@ -110,7 +112,9 @@ class IoTDBTableIngestionBenchmarkIT {
 
   @Container
   static final GenericContainer<?> IOTDB =
-      new GenericContainer<>(DockerImageName.parse("apache/iotdb:2.0.8-standalone"))
+      new GenericContainer<>(
+              DockerImageName.parse(
+                  System.getProperty("iotdb.test.image", "apache/iotdb:2.0.11-standalone")))
           .withExposedPorts(6667)
           // IoTDB binds its client RPC service to dn_rpc_address (default 127.0.0.1), so it would
           // only listen on the container loopback and reject the Testcontainers port-mapped session
@@ -400,10 +404,26 @@ class IoTDBTableIngestionBenchmarkIT {
         uniqueDatabase(), new TenantId(UUID.fromString("55555555-5555-5555-5555-555555555501")));
   }
 
+  @AfterEach
+  void dropCreatedDatabases() throws Exception {
+    // Every test provisions its own database. Drop them once the test is done: the shared
+    // container has a fixed region memory budget, and leaving dozens of databases behind makes
+    // later tests in the class fail schema-region creation ("Total allocated memory for direct
+    // buffer ... is greater than limit mem cost") and time out on their first write.
+    try (ITableSessionPool pool = newPool(null);
+        ITableSession session = pool.getSession()) {
+      for (String database : createdDatabases) {
+        session.executeNonQueryStatement("DROP DATABASE IF EXISTS " + database);
+      }
+    }
+  }
+
   private String uniqueDatabase() {
     // IoTDB caps database names at 64 chars; keep the prefix short and append a trimmed UUID.
     String shortUuid = UUID.randomUUID().toString().replace("-", "").substring(0, 16);
-    return "tb_bench_tc1_" + shortUuid;
+    String database = "tb_bench_tc1_" + shortUuid;
+    createdDatabases.add(database);
+    return database;
   }
 
   private static java.util.concurrent.ThreadFactory saverThreadFactory() {
diff --git a/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableLatestDaoIT.java b/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableLatestDaoIT.java
index a4c329a..880ea2a 100644
--- a/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableLatestDaoIT.java
+++ b/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableLatestDaoIT.java
@@ -24,6 +24,7 @@ import org.apache.iotdb.isession.pool.ITableSessionPool;
 import org.apache.iotdb.session.pool.TableSessionPoolBuilder;
 
 import com.google.common.util.concurrent.ListenableFuture;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.testcontainers.containers.GenericContainer;
@@ -64,6 +65,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 @Tag("integration")
 @Testcontainers(disabledWithoutDocker = true)
 class IoTDBTableLatestDaoIT {
+  private final List<String> createdDatabases = new ArrayList<>();
   // Cold testcontainer first writes/reads are slower than a warm production node, so the
   // per-future assertion timeout is generous; production throughput is covered elsewhere.
   private static final int FUTURE_TIMEOUT_SECONDS = 30;
@@ -73,7 +75,9 @@ class IoTDBTableLatestDaoIT {
 
   @Container
   static final GenericContainer<?> IOTDB =
-      new GenericContainer<>(DockerImageName.parse("apache/iotdb:2.0.8-standalone"))
+      new GenericContainer<>(
+              DockerImageName.parse(
+                  System.getProperty("iotdb.test.image", "apache/iotdb:2.0.11-standalone")))
           .withExposedPorts(6667)
           // IoTDB binds its client RPC service to dn_rpc_address (default 127.0.0.1); bind to all
           // interfaces so the Testcontainers port-mapped session handshake succeeds.
@@ -1096,12 +1100,28 @@ class IoTDBTableLatestDaoIT {
         new TestEntityId(UUID.fromString(entityId), EntityType.DEVICE));
   }
 
+  @AfterEach
+  void dropCreatedDatabases() throws Exception {
+    // Every test provisions its own database. Drop them once the test is done: the shared
+    // container has a fixed region memory budget, and leaving dozens of databases behind makes
+    // later tests in the class fail schema-region creation ("Total allocated memory for direct
+    // buffer ... is greater than limit mem cost") and time out on their first write.
+    try (ITableSessionPool pool = newPool(null);
+        ITableSession session = pool.getSession()) {
+      for (String database : createdDatabases) {
+        session.executeNonQueryStatement("DROP DATABASE IF EXISTS " + database);
+      }
+    }
+  }
+
   private String uniqueDatabase(String prefix) {
     // IoTDB caps database names at 64 chars; keep the per-test prefix short and append a trimmed
     // UUID so the total length stays well within the limit.
     String shortPrefix = prefix.length() > 12 ? prefix.substring(0, 12) : prefix;
     String shortUuid = UUID.randomUUID().toString().replace("-", "").substring(0, 16);
-    return "tb_lt_" + shortPrefix + "_" + shortUuid;
+    String database = "tb_lt_" + shortPrefix + "_" + shortUuid;
+    createdDatabases.add(database);
+    return database;
   }
 
   private TestTsKvEntry entry(long ts, String key, DataType dataType, Object value) {
diff --git a/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableTimeseriesAggregationIT.java b/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableTimeseriesAggregationIT.java
index aad140b..4bf100d 100644
--- a/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableTimeseriesAggregationIT.java
+++ b/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableTimeseriesAggregationIT.java
@@ -23,6 +23,7 @@ import org.apache.iotdb.isession.pool.ITableSessionPool;
 import org.apache.iotdb.session.pool.TableSessionPoolBuilder;
 
 import com.google.common.util.concurrent.ListenableFuture;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.testcontainers.containers.GenericContainer;
@@ -55,18 +56,19 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
- * Real-Docker integration test that proves the IoTDB 2.0.8 Table Mode native three-argument {@code
+ * Real-Docker integration test that proves the IoTDB 2.0.11 Table Mode native three-argument {@code
  * date_bin(<interval>ms, time, <startTs>)} + {@code GROUP BY} time-bucketed aggregation path
  * matches ThingsBoard 4.3.1.2's contract: buckets anchored at {@code startTs} (not epoch 1970),
  * entries stamped at the bucket midpoint, every non-empty bucket returned ascending regardless of
  * query order/limit, and typed COUNT semantics -- all against hand-computed expected values. Reuses
  * the testcontainer harness from {@link IoTDBTableTimeseriesDaoIT}: {@code
- * apache/iotdb:2.0.8-standalone}, {@code dn_rpc_address=0.0.0.0}, exposed port 6667, short-prefix
+ * apache/iotdb:2.0.11-standalone}, {@code dn_rpc_address=0.0.0.0}, exposed port 6667, short-prefix
  * unique database, schema bootstrap from {@code schema-iotdb-table.sql}.
  */
 @Tag("integration")
 @Testcontainers(disabledWithoutDocker = true)
 class IoTDBTableTimeseriesAggregationIT {
+  private final List<String> createdDatabases = new ArrayList<>();
   private static final int FUTURE_TIMEOUT_SECONDS = 30;
   private static final Duration IOTDB_STARTUP_TIMEOUT = Duration.ofMinutes(3);
   private static final Duration IOTDB_READY_TIMEOUT = Duration.ofSeconds(60);
@@ -75,7 +77,9 @@ class IoTDBTableTimeseriesAggregationIT {
 
   @Container
   static final GenericContainer<?> IOTDB =
-      new GenericContainer<>(DockerImageName.parse("apache/iotdb:2.0.8-standalone"))
+      new GenericContainer<>(
+              DockerImageName.parse(
+                  System.getProperty("iotdb.test.image", "apache/iotdb:2.0.11-standalone")))
           .withExposedPorts(6667)
           .withEnv("dn_rpc_address", "0.0.0.0")
           .waitingFor(Wait.forListeningPort().withStartupTimeout(IOTDB_STARTUP_TIMEOUT));
@@ -1109,10 +1113,26 @@ class IoTDBTableTimeseriesAggregationIT {
         new TestEntityId(UUID.fromString(entityId), EntityType.DEVICE));
   }
 
+  @AfterEach
+  void dropCreatedDatabases() throws Exception {
+    // Every test provisions its own database. Drop them once the test is done: the shared
+    // container has a fixed region memory budget, and leaving dozens of databases behind makes
+    // later tests in the class fail schema-region creation ("Total allocated memory for direct
+    // buffer ... is greater than limit mem cost") and time out on their first write.
+    try (ITableSessionPool pool = newPool(null);
+        ITableSession session = pool.getSession()) {
+      for (String database : createdDatabases) {
+        session.executeNonQueryStatement("DROP DATABASE IF EXISTS " + database);
+      }
+    }
+  }
+
   private String uniqueDatabase(String prefix) {
     String shortPrefix = prefix.length() > 12 ? prefix.substring(0, 12) : prefix;
     String shortUuid = UUID.randomUUID().toString().replace("-", "").substring(0, 16);
-    return "tb_it_" + shortPrefix + "_" + shortUuid;
+    String database = "tb_it_" + shortPrefix + "_" + shortUuid;
+    createdDatabases.add(database);
+    return database;
   }
 
   private TestTsKvEntry entry(long ts, String key, DataType dataType, Object value) {
diff --git a/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableTimeseriesDaoIT.java b/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableTimeseriesDaoIT.java
index 46e0aad..cc9bf77 100644
--- a/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableTimeseriesDaoIT.java
+++ b/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableTimeseriesDaoIT.java
@@ -25,6 +25,7 @@ import org.apache.iotdb.session.pool.TableSessionPoolBuilder;
 
 import com.google.common.util.concurrent.ListenableFuture;
 import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
@@ -59,7 +60,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
- * Integration tests for the IoTDB Table Mode timeseries DAO against a real IoTDB 2.0.8 container:
+ * Integration tests for the IoTDB Table Mode timeseries DAO against a real IoTDB 2.0.11 container:
  * the WRITE path (verified by reading the telemetry table back through raw table-session SQL) plus
  * the RAW (non-aggregated) READ path, a millisecond time-bucketed aggregation smoke read and the
  * DELETE path exercised through the DAO.
@@ -67,6 +68,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 @Tag("integration")
 @Testcontainers(disabledWithoutDocker = true)
 class IoTDBTableTimeseriesDaoIT {
+  private final List<String> createdDatabases = new ArrayList<>();
   // Cold testcontainer first writes are slower than a warm production node, so the per-future
   // assertion timeout is generous; production throughput is covered elsewhere, not here.
   private static final int FUTURE_TIMEOUT_SECONDS = 30;
@@ -78,7 +80,9 @@ class IoTDBTableTimeseriesDaoIT {
 
   @Container
   static final GenericContainer<?> IOTDB =
-      new GenericContainer<>(DockerImageName.parse("apache/iotdb:2.0.8-standalone"))
+      new GenericContainer<>(
+              DockerImageName.parse(
+                  System.getProperty("iotdb.test.image", "apache/iotdb:2.0.11-standalone")))
           .withExposedPorts(6667)
           // IoTDB binds its client RPC service to dn_rpc_address (default 127.0.0.1), so it would
           // only listen on the container loopback and reject the Testcontainers port-mapped session
@@ -101,7 +105,7 @@ class IoTDBTableTimeseriesDaoIT {
   }
 
   /**
-   * Pins the IoTDB 2.0.8 engine behavior that a database-bound table-session pool can bootstrap a
+   * Pins the IoTDB 2.0.11 engine behavior that a database-bound table-session pool can bootstrap a
    * not-yet-existing database. If a future IoTDB image changes that behavior, this test fails
    * before first boot breaks in production.
    */
@@ -628,12 +632,28 @@ class IoTDBTableTimeseriesDaoIT {
         new TestEntityId(UUID.fromString(entityId), EntityType.DEVICE));
   }
 
+  @AfterEach
+  void dropCreatedDatabases() throws Exception {
+    // Every test provisions its own database. Drop them once the test is done: the shared
+    // container has a fixed region memory budget, and leaving dozens of databases behind makes
+    // later tests in the class fail schema-region creation ("Total allocated memory for direct
+    // buffer ... is greater than limit mem cost") and time out on their first write.
+    try (ITableSessionPool pool = newPool(null);
+        ITableSession session = pool.getSession()) {
+      for (String database : createdDatabases) {
+        session.executeNonQueryStatement("DROP DATABASE IF EXISTS " + database);
+      }
+    }
+  }
+
   private String uniqueDatabase(String prefix) {
     // IoTDB caps database names at 64 chars; keep the per-test prefix short and
     // append a trimmed UUID so the total length stays well within the limit.
     String shortPrefix = prefix.length() > 12 ? prefix.substring(0, 12) : prefix;
     String shortUuid = UUID.randomUUID().toString().replace("-", "").substring(0, 16);
-    return "tb_it_" + shortPrefix + "_" + shortUuid;
+    String database = "tb_it_" + shortPrefix + "_" + shortUuid;
+    createdDatabases.add(database);
+    return database;
   }
 
   private int assertTelemetryRows(
diff --git a/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableTimeseriesDaoTest.java b/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableTimeseriesDaoTest.java
index 66fa857..b3dc70a 100644
--- a/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableTimeseriesDaoTest.java
+++ b/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableTimeseriesDaoTest.java
@@ -2746,9 +2746,9 @@ class IoTDBTableTimeseriesDaoTest {
         case "sum_double" -> sumDouble == null;
         case "min_long" -> minLong == null;
         case "max_long" -> maxLong == null;
-          // MAX(time) is NULL iff the bounded window matched zero rows (time is never null). A real
-          // empty calendar bucket (emptyAggRow) returns one row with MAX(time) NULL; every other
-          // bucket has matching data, so its max_ts is non-null.
+        // MAX(time) is NULL iff the bounded window matched zero rows (time is never null). A real
+        // empty calendar bucket (emptyAggRow) returns one row with MAX(time) NULL; every other
+        // bucket has matching data, so its max_ts is non-null.
         case "max_ts" -> emptyAgg;
         default -> true;
       };
diff --git a/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableTtlIT.java b/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableTtlIT.java
index bd6cf4f..fde0ac7 100644
--- a/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableTtlIT.java
+++ b/iotdb-thingsboard-table/src/test/java/org/apache/iotdb/extras/thingsboard/table/IoTDBTableTtlIT.java
@@ -23,6 +23,7 @@ import org.apache.iotdb.isession.SessionDataSet;
 import org.apache.iotdb.isession.pool.ITableSessionPool;
 import org.apache.iotdb.session.pool.TableSessionPoolBuilder;
 
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.testcontainers.containers.GenericContainer;
@@ -34,6 +35,7 @@ import org.testcontainers.utility.DockerImageName;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 import java.time.Duration;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
@@ -42,8 +44,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
- * Validates the table-level TTL mechanism against real IoTDB 2.0.8. IoTDB Table Mode TTL is a table
- * property expressed in milliseconds; this IT proves the two operator-facing paths the README
+ * Validates the table-level TTL mechanism against real IoTDB 2.0.11. IoTDB Table Mode TTL is a
+ * table property expressed in milliseconds; this IT proves the two operator-facing paths the README
  * documents work as described:
  *
  * <ul>
@@ -61,6 +63,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 @Tag("integration")
 @Testcontainers(disabledWithoutDocker = true)
 class IoTDBTableTtlIT {
+  private final List<String> createdDatabases = new ArrayList<>();
   private static final Duration IOTDB_STARTUP_TIMEOUT = Duration.ofMinutes(3);
   private static final Duration IOTDB_READY_TIMEOUT = Duration.ofSeconds(60);
   private static final Duration IOTDB_READY_POLL_INTERVAL = Duration.ofMillis(500);
@@ -72,7 +75,9 @@ class IoTDBTableTtlIT {
 
   @Container
   static final GenericContainer<?> IOTDB =
-      new GenericContainer<>(DockerImageName.parse("apache/iotdb:2.0.8-standalone"))
+      new GenericContainer<>(
+              DockerImageName.parse(
+                  System.getProperty("iotdb.test.image", "apache/iotdb:2.0.11-standalone")))
           .withExposedPorts(6667)
           // IoTDB binds its client RPC service to dn_rpc_address (default 127.0.0.1); bind to all
           // interfaces so the Testcontainers port-mapped session handshake succeeds.
@@ -208,9 +213,25 @@ class IoTDBTableTtlIT {
         "IoTDB did not accept table-session statements within " + IOTDB_READY_TIMEOUT, lastFailure);
   }
 
+  @AfterEach
+  void dropCreatedDatabases() throws Exception {
+    // Every test provisions its own database. Drop them once the test is done: the shared
+    // container has a fixed region memory budget, and leaving dozens of databases behind makes
+    // later tests in the class fail schema-region creation ("Total allocated memory for direct
+    // buffer ... is greater than limit mem cost") and time out on their first write.
+    try (ITableSessionPool pool = newPool(null);
+        ITableSession session = pool.getSession()) {
+      for (String database : createdDatabases) {
+        session.executeNonQueryStatement("DROP DATABASE IF EXISTS " + database);
+      }
+    }
+  }
+
   private String uniqueDatabase(String prefix) {
     String shortPrefix = prefix.length() > 12 ? prefix.substring(0, 12) : prefix;
     String shortUuid = UUID.randomUUID().toString().replace("-", "").substring(0, 16);
-    return "tb_it_" + shortPrefix + "_" + shortUuid;
+    String database = "tb_it_" + shortPrefix + "_" + shortUuid;
+    createdDatabases.add(database);
+    return database;
   }
 }
diff --git a/mybatis-generator/README-zh.md b/mybatis-generator/README-zh.md
index c91e773..bee354a 100644
--- a/mybatis-generator/README-zh.md
+++ b/mybatis-generator/README-zh.md
@@ -19,38 +19,76 @@
 
 -->
 
-# mybatis-generator-plugin
+# IoTDB MyBatis Generator 插件
 
-- 把该项目 `clone` 下来之后,在本地执行 `mvn clean install` 或者 `mvn clean deploy` (`deploy` 需要修改 `pom` 中的 `distributionManagement`)【已经上传 `Maven` 中央仓库,所以此步骤不在需要】
+为 IoTDB **表模型**生成 Model、Mapper 和 XML,提供批量插入、Lombok、序列化、Swagger 注释和 JDBC 类型映射。要求 **JDK 17+、IoTDB/JDBC 2.0.11、MBG 1.4.2**。插件版本属于 Extras,本仓库仍为 `2.0.4-SNAPSHOT`。
 
-- 在要生成的项目的 `pom` 文件中添加如下配置:
+## 安装与配置
+
+在仓库根目录安装生成插件和运行时适配:
+
+```sh
+mvn -pl mybatis-generator,mybatis-support -am clean install
+```
+
+`mvn package -Pwith-mybatis` 还会生成 `distributions/target/apache-iotdb-<version>-mybatis-generator-plugin-bin.zip`,其中同时打包生成插件和 `mybatis-support` 运行时 jar。
+
+生成时,`mybatis-generator-maven-plugin:1.4.2` 的 `dependencies` 中必须包含:
+
+| 依赖 | 版本 |
+|---|---|
+| `org.apache.iotdb:mybatis-generator-plugin` | `2.0.4-SNAPSHOT` |
+| `org.apache.iotdb:iotdb-jdbc` | `2.0.11` |
+
+无需配置机器相关的 `classPathEntry`。应用运行时还要依赖 `org.apache.iotdb:mybatis-support:2.0.4-SNAPSHOT` 并[注册查询拦截器](../mybatis-support/README.md)。2.0.11 表模型驱动的 prepared query 路径需要这一适配。
+
+参考[完整生成配置](../examples/mybatis-generator/src/main/resources/generatorConfig.xml),设置 JDBC URL、凭据、表名及输出路径。先创建表,再从应用项目目录执行:
+
+```sh
+mvn mybatis-generator:generate
+```
+
+生成会读取真实元数据并可能覆盖已有文件;检查差异后再使用。
+
+## 键、类型与 SQL 语义
 
 ```xml
-<build>
-    <plugins>
-        <plugin>
-            <groupId>org.mybatis.generator</groupId>
-            <artifactId>mybatis-generator-maven-plugin</artifactId>
-            <version>1.4.2</version>
-            <dependencies>
-                <dependency>
-                    <groupId>org.apache.iotdb</groupId>
-                    <artifactId>mybatis-generator-plugin</artifactId>
-                    <version>2.0.3</version>
-                </dependency>
-            </dependencies>
-            <configuration>
-                <verbose>true</verbose>
-                <overwrite>true</overwrite>
-                <configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
-            </configuration>
-        </plugin>
-    </plugins>
-</build>
+<plugin type="org.apache.iotdb.mybatis.plugin.BatchInsertPlugin">
+    <property name="batchSize" value="500"/>
+</plugin>
+<plugin type="org.mybatis.generator.plugins.VirtualPrimaryKeyPlugin"/>
+<plugin type="org.apache.iotdb.mybatis.plugin.IoTDBKeyPlugin"/>
 ```
 
-- `configurationFile` 配置 `generatorConfig.xml` 文件的位置,其内容在本项目的 `src/main/resources` 有一个模板供参考,`copy` 其内容放到相应的位置
+- 行键由 **TIME 与全部 TAG** 组成,`virtualKeyColumns` 必须与表结构一致。`IoTDBKeyPlugin` 在查询、删除的键谓词中为 NULL TAG 生成 `IS NULL`。
+- `IoTDBJavaTypeResolver` 将 TIMESTAMP 映射为 **Long**;数值单位跟随服务端 ms/us/ns,适配层不转换单位。FLOAT 配置为 `java.lang.Float`。不要用 `Date` 承接高精度时间戳。
+- DATE 使用 `LocalDate + IoTDBLocalDateTypeHandler`,BLOB 使用 `byte[] + IoTDBBlobTypeHandler`。通过 `columnOverride` 同时生成参数及结果映射,详见[运行时适配文档](../mybatis-support/README.md)。
+- 设置 `enableUpdateByPrimaryKey=false` 和 `enableUpdateByExample=false`。2.0.11 的 UPDATE 只支持 ATTRIBUTE,且谓词中不能出现 `time`,MBG 按主键生成的 UPDATE 无法执行;若仍开启,`IoTDBKeyPlugin` 会跳过这些语句并输出生成警告。修改 FIELD 要按相同行键执行 INSERT。省略或为 NULL 的 FIELD 不会清除已有值。
+- ATTRIBUTE 属于设备,更新会影响该设备所有时间点。普通关系数据库的通用 UPDATE 不能直接套用。
+- 批量 SQL 保留 MBG 的标识符转义和 TypeHandler。保留字使用 `delimitIdentifiers` / `delimitAllColumns`。
+- 示例使用 `ignoreQualifiersAtRuntime=true`:生成读取配置中的 schema,运行时由 JDBC URL 选择数据库。
+- Lombok/Swagger 注解依赖需要放在应用 classpath 中;Lombok 同时覆盖主键类、普通模型和 BLOB 子类。
+
+## 批量行为与迁移
+
+调用 `mapper.batchInsert(records)`。默认方法先校验整份列表,再按 **500 行**拆分 SQL;插件属性 `batchSize` 必须为正整数。宽表或大 BLOB 应减小批次,它限制行数,不限制字节数。
+
+| 输入/配置 | 行为 |
+|---|---|
+| 空列表 | 返回 0,不访问数据库 |
+| NULL 列表或 NULL 元素 | 写入前抛出 `IllegalArgumentException` |
+| identity/autoincrement/generated-always 列 | 不参与批量插入 |
+| `incrementField` | 保持旧版排除列兼容 |
+| 关闭 INSERT | 不生成批量方法/XML |
+| 所有列都被排除 | 不生成批量方法/XML,并报告警告 |
+| BLOB 分层模型 | 使用包含全部字段的模型类型 |
+
+`batchInsertRows(@Param("records") List<T>)` 是内部单批映射方法,直接调用会绕过校验和拆批。**从旧版升级时,必须同时重新生成 Mapper 接口和 XML**,不能只替换其中一个。
+
+示例启用 `UnmergeableXmlMappersPlugin`,在关闭注释时也直接替换生成 XML,避免重复生成累积同名语句。手写 SQL 应单独保存,或在重新生成前检查并保留。
+
+2.0.11 JDBC 的影响行数为 -1(未知)。任一批次返回未知时,总返回值保持 -1,否则累加已知数量;不要用 `result > 0` 判断成功。SQL 失败会抛异常,但之前的批次可能已写入。IoTDB JDBC 不提供事务回滚,`@Transactional` 不能把多次写入变成原子操作。
 
-- 修改 `generatorConfig.xml` 中 想用的内容,主要是:`jdbcConnection`、`javaModelGenerator`、`sqlMapGenerator`、`javaClientGenerator`、`table`
+实际查询应限制时间/设备范围和返回条数。检入的简单示例将 `selectAll` 限制为 1000 条;MBG 标准模板不会自动添加该限制,重新生成后应检查查询范围。
 
-- 在项目的 `pom` 所在的地方执行命令:`mvn mybatis-generator:generate` 生成相应的 `Java` 类和 `mapper` 文件
+集成测试和已知服务端限制参见[可运行示例](../examples/mybatis-generator/README.md)和[MyBatis-Plus 示例](../examples/mybatisplus-generator/README.md)。本插件与 MyBatis-Plus 的生成器是两套独立集成。
diff --git a/mybatis-generator/README.md b/mybatis-generator/README.md
index 4f60b9c..713164b 100644
--- a/mybatis-generator/README.md
+++ b/mybatis-generator/README.md
@@ -19,38 +19,98 @@
 
 -->
 
-# mybatis-generator-plugin
+# IoTDB MyBatis Generator plugin
 
-- After 'clone' the project, execute 'mvn clean install' or 'mvn clean deploy' locally ('deploy' needs to modify 'distributionManagement' in 'pom'). This step is not necessary as it has already been uploaded to the Maven central repository
+Generates MyBatis models, mapper interfaces and XML for the IoTDB **table model**. Features include `batchInsert`, Lombok models, serializable models, Swagger comments and configurable JDBC-to-Java type mapping.
 
-- Add the following configuration to the 'pom' file of the project to be generated:
+## Prerequisites
+
+Use JDK 17+, IoTDB/JDBC 2.0.11, and MyBatis Generator 1.4.2. The plugin artifact belongs to Extras (`2.0.4-SNAPSHOT` in this checkout); its version does not track the IoTDB driver version.
+
+From the repository root:
+
+```sh
+mvn -pl mybatis-generator,mybatis-support -am clean install
+```
+
+`mvn package -Pwith-mybatis` additionally builds `distributions/target/apache-iotdb-<version>-mybatis-generator-plugin-bin.zip`, which bundles the generator plugin jar together with the `mybatis-support` runtime jar.
+
+## Configure the generator
+
+Both the plugin and JDBC driver must be in the generator's plugin classloader. No absolute `classPathEntry` or manually copied JDBC jar is needed:
+
+```xml
+<plugin>
+    <groupId>org.mybatis.generator</groupId>
+    <artifactId>mybatis-generator-maven-plugin</artifactId>
+    <version>1.4.2</version>
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.iotdb</groupId>
+            <artifactId>mybatis-generator-plugin</artifactId>
+            <version>2.0.4-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.iotdb</groupId>
+            <artifactId>iotdb-jdbc</artifactId>
+            <version>2.0.11</version>
+        </dependency>
+    </dependencies>
+    <configuration>
+        <configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
+        <verbose>true</verbose>
+        <overwrite>true</overwrite>
+    </configuration>
+</plugin>
+```
+
+Use the [example configuration](../examples/mybatis-generator/src/main/resources/generatorConfig.xml) as a starting point. Set the JDBC URL to `jdbc:iotdb://127.0.0.1:6667/test?sql_dialect=table`, update credentials, output packages and table names, and create the target database/table before generation.
+
+Run from the consuming project's directory:
+
+```sh
+mvn mybatis-generator:generate
+```
+
+Generation reads database metadata and can overwrite generated sources. Review the diff before incorporating output into application code.
+
+## IoTDB-specific mapping
+
+Applications also need [mybatis-support](../mybatis-support/README.md), including its query interceptor and DATE/BLOB handlers. The query interceptor is required for MyBatis's prepared query path with the 2.0.11 table driver.
 
 ```xml
-<build>
-    <plugins>
-        <plugin>
-            <groupId>org.mybatis.generator</groupId>
-            <artifactId>mybatis-generator-maven-plugin</artifactId>
-            <version>1.4.2</version>
-            <dependencies>
-                <dependency>
-                    <groupId>org.apache.iotdb</groupId>
-                    <artifactId>mybatis-generator-plugin</artifactId>
-                    <version>2.0.3</version>
-                </dependency>
-            </dependencies>
-            <configuration>
-                <verbose>true</verbose>
-                <overwrite>true</overwrite>
-                <configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
-            </configuration>
-        </plugin>
-    </plugins>
-</build>
+<plugin type="org.apache.iotdb.mybatis.plugin.BatchInsertPlugin">
+    <property name="batchSize" value="500"/>
+</plugin>
+<plugin type="org.mybatis.generator.plugins.VirtualPrimaryKeyPlugin"/>
+<plugin type="org.apache.iotdb.mybatis.plugin.IoTDBKeyPlugin"/>
 ```
 
-- The location of the `configurationFile` configuration `generatorConfig. xml` file can be found in the `src/main/resources` template of this project for reference` Copy its content and place it in the corresponding location
+- `IoTDBJavaTypeResolver` maps TIMESTAMP to **Long**. Values use the server's configured ms/us/ns precision without conversion. Set `jdbcType.FLOAT=java.lang.Float`; do not map high-precision timestamps to `Date`.
+- The logical row identity is **TIME plus every TAG**. Keep `virtualKeyColumns` synchronized with the real schema; `IoTDBKeyPlugin` emits `IS NULL` for nullable TAG components in generated SELECT/DELETE predicates. ATTRIBUTE and FIELD columns are not keys.
+- Disable `enableUpdateByPrimaryKey` and `enableUpdateByExample`. IoTDB 2.0.11 UPDATE changes ATTRIBUTE columns only and rejects `time` in its predicate, so MBG's key-based UPDATE statements cannot run; `IoTDBKeyPlugin` drops them and reports a generator warning if they are left enabled. To change FIELD values, INSERT the same key and the desired fields; omitted/null fields do not erase existing values. ATTRIBUTE updates affect the device across timestamps.
+- Add DATE/BLOB `columnOverride` entries from [runtime support](../mybatis-support/README.md), so the same handlers apply to inserts, batch parameters and result maps.
+- Use `delimitIdentifiers` / `delimitAllColumns` for SQL identifiers requiring quotes. Batch SQL uses MBG's formatting helpers and preserves configured handlers and escaping.
+- Lombok/Swagger plugins require the corresponding annotation dependencies in the consuming application. Lombok is applied to primary-key and BLOB model classes as well as base records.
+
+The examples use `ignoreQualifiersAtRuntime=true`: generation reads the configured schema, while runtime SQL uses the database selected by the JDBC URL.
+
+## Batch behavior and migration
+
+Call `mapper.batchInsert(records)`. The generated default method validates the entire list before writing and splits it into at most **500 rows** per SQL statement. `batchSize` must be a positive integer; lower it for wide rows, large BLOBs or server request limits. This is a row limit, not a byte-size limit.
+
+- Empty list: returns 0 and issues no SQL.
+- Null list or null element: throws `IllegalArgumentException` before any chunk is sent.
+- Identity, autoincrement, generated-always and legacy `incrementField` columns are excluded.
+- Disabled inserts or a table with no insertable columns produce no batch method/statement; the latter reports a generation warning.
+- Models with a separate BLOB subclass use the all-fields type.
+
+`batchInsertRows(@Param("records") List<T>)` is the internal mapped statement. **Regenerate the mapper interface and XML together** when migrating from the previous mapped `batchInsert` method. Calling the internal helper directly bypasses validation and chunking.
+
+The sample configurations enable `UnmergeableXmlMappersPlugin` so reruns replace generated XML even when comments are suppressed, instead of accumulating duplicate statements. Preserve handwritten SQL separately or review it before regeneration.
+
+The driver reports affected-row count as -1 (unknown); the public batch method preserves -1 if any chunk has an unknown count, otherwise it sums known counts. SQL failures propagate as exceptions. Earlier chunks may already have been written when a later chunk fails: IoTDB JDBC 2.0.11 does not implement rollback, and MyBatis/Spring transaction annotations cannot make the batch atomic.
 
-- Modify the content you want to use in 'generatorConfig. xml', mainly by:` jdbcConnection`、`javaModelGenerator`、`sqlMapGenerator`、`javaClientGenerator`、`table`
+Bound generated `selectAll`/Example queries in application code with time/device predicates and a limit. The checked-in simple example caps `selectAll` at 1000; MBG's standard templates do not add this cap automatically.
 
-- Execute the command at the location of the 'pom' in the project:` Mvn mybatis generator: generate generates corresponding Java classes and mapper files
+See the [runnable MyBatis example](../examples/mybatis-generator/README.md) and [MyBatis-Plus example](../examples/mybatisplus-generator/README.md) for integration tests and known server limitations. This plugin integrates with MyBatis Generator; the MyBatis-Plus example uses its own generator.
diff --git a/mybatis-generator/pom.xml b/mybatis-generator/pom.xml
index 26a108e..7df748d 100644
--- a/mybatis-generator/pom.xml
+++ b/mybatis-generator/pom.xml
@@ -19,10 +19,21 @@
         </license>
     </licenses>
     <dependencies>
+        <dependency>
+            <groupId>org.mybatis</groupId>
+            <artifactId>mybatis</artifactId>
+            <version>3.5.19</version>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.mybatis.generator</groupId>
             <artifactId>mybatis-generator-core</artifactId>
             <version>1.4.2</version>
         </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 </project>
diff --git a/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/plugin/BatchInsertPlugin.java b/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/plugin/BatchInsertPlugin.java
index cb24f3c..f3cf80c 100644
--- a/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/plugin/BatchInsertPlugin.java
+++ b/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/plugin/BatchInsertPlugin.java
@@ -29,39 +29,88 @@ import org.mybatis.generator.api.dom.xml.Attribute;
 import org.mybatis.generator.api.dom.xml.Document;
 import org.mybatis.generator.api.dom.xml.TextElement;
 import org.mybatis.generator.api.dom.xml.XmlElement;
+import org.mybatis.generator.codegen.mybatis3.MyBatis3FormattingUtilities;
 
 import java.util.List;
 import java.util.Set;
 import java.util.TreeSet;
+import java.util.stream.Collectors;
 
 public class BatchInsertPlugin extends PluginAdapter {
+  private int batchSize = 500;
+  private List<String> warnings;
 
   @Override
   public boolean clientGenerated(Interface interfaze, IntrospectedTable introspectedTable) {
-    batchInsertMethod(interfaze, introspectedTable);
+    if (canInsert(introspectedTable)) {
+      batchInsertMethod(interfaze, introspectedTable);
+    }
 
     return super.clientGenerated(interfaze, introspectedTable);
   }
 
   @Override
   public boolean sqlMapDocumentGenerated(Document document, IntrospectedTable introspectedTable) {
-    addBatchInsertXml(document, introspectedTable);
+    if (canInsert(introspectedTable)) {
+      addBatchInsertXml(document, introspectedTable);
+    }
     return super.sqlMapDocumentGenerated(document, introspectedTable);
   }
 
   @Override
-  public boolean validate(List<String> list) {
-    return true;
+  public boolean validate(List<String> warnings) {
+    this.warnings = warnings;
+    try {
+      batchSize = Integer.parseInt(properties.getProperty("batchSize", "500"));
+      if (batchSize > 0) {
+        return true;
+      }
+    } catch (NumberFormatException ignored) {
+      // Report the invalid plugin setting through MBG's configuration warnings.
+    }
+    warnings.add("BatchInsertPlugin: batchSize must be a positive integer");
+    return false;
+  }
+
+  @Override
+  public void initialized(IntrospectedTable table) {
+    if (table.getTableConfiguration().isInsertStatementEnabled()
+        && insertColumns(table).isEmpty()) {
+      warnings.add("BatchInsertPlugin: no insertable columns in " + table.getFullyQualifiedTable());
+    }
+  }
+
+  private boolean canInsert(IntrospectedTable table) {
+    return table.getTableConfiguration().isInsertStatementEnabled()
+        && !insertColumns(table).isEmpty();
+  }
+
+  private List<IntrospectedColumn> insertColumns(IntrospectedTable table) {
+    String incrementField = table.getTableConfigurationProperty("incrementField");
+    return table.getAllColumns().stream()
+        .filter(
+            column ->
+                !column.isIdentity()
+                    && !column.isAutoIncrement()
+                    && !column.isGeneratedAlways()
+                    && !column.isGeneratedColumn())
+        .filter(
+            column ->
+                incrementField == null
+                    || !column.getActualColumnName().equalsIgnoreCase(incrementField.trim()))
+        .collect(Collectors.toList());
   }
 
   private void batchInsertMethod(Interface interfaze, IntrospectedTable introspectedTable) {
     Set<FullyQualifiedJavaType> importedTypes = new TreeSet<>();
     importedTypes.add(FullyQualifiedJavaType.getNewListInstance());
-    importedTypes.add(new FullyQualifiedJavaType(introspectedTable.getBaseRecordType()));
+    importedTypes.add(new FullyQualifiedJavaType("org.apache.ibatis.annotations.Param"));
+    FullyQualifiedJavaType recordType = introspectedTable.getRules().calculateAllFieldsClass();
+    importedTypes.add(recordType);
 
     Method ibsmethod = new Method("batchInsert");
     ibsmethod.setVisibility(JavaVisibility.PUBLIC);
-    ibsmethod.setAbstract(true);
+    ibsmethod.setDefault(true);
 
     FullyQualifiedJavaType ibsReturnType = FullyQualifiedJavaType.getIntInstance();
 
@@ -70,41 +119,47 @@ public class BatchInsertPlugin extends PluginAdapter {
     ibsmethod.setName("batchInsert");
 
     FullyQualifiedJavaType paramType = FullyQualifiedJavaType.getNewListInstance();
-    FullyQualifiedJavaType paramListType;
-    paramListType = new FullyQualifiedJavaType(introspectedTable.getBaseRecordType());
-    paramType.addTypeArgument(paramListType);
-    ibsmethod.addParameter(new Parameter(paramType, "records", "@Param(\"records\")"));
+    paramType.addTypeArgument(recordType);
+    ibsmethod.addParameter(new Parameter(paramType, "records"));
+    ibsmethod.addBodyLine(
+        "if (records == null || records.stream().anyMatch(java.util.Objects::isNull)) {");
+    ibsmethod.addBodyLine(
+        "throw new IllegalArgumentException(\"records and its elements must not be null\");");
+    ibsmethod.addBodyLine("}");
+    ibsmethod.addBodyLine("int result = 0;");
+    ibsmethod.addBodyLine("for (int start = 0; start < records.size();) {");
+    ibsmethod.addBodyLine("int end = start + Math.min(" + batchSize + ", records.size() - start);");
+    ibsmethod.addBodyLine("int count = batchInsertRows(records.subList(start, end));");
+    ibsmethod.addBodyLine("result = count < 0 || result < 0 ? -1 : result + count;");
+    ibsmethod.addBodyLine("start = end;");
+    ibsmethod.addBodyLine("}");
+    ibsmethod.addBodyLine("return result;");
     interfaze.addImportedTypes(importedTypes);
 
     interfaze.addMethod(ibsmethod);
+    Method rowsMethod = new Method("batchInsertRows");
+    rowsMethod.setVisibility(JavaVisibility.PUBLIC);
+    rowsMethod.setAbstract(true);
+    rowsMethod.setReturnType(FullyQualifiedJavaType.getIntInstance());
+    rowsMethod.addParameter(new Parameter(paramType, "records", "@Param(\"records\")"));
+    rowsMethod.addJavaDocLine("/** Internal single-batch statement; call batchInsert instead. */");
+    interfaze.addMethod(rowsMethod);
   }
 
   private void addBatchInsertXml(Document document, IntrospectedTable introspectedTable) {
-    List<IntrospectedColumn> columns = introspectedTable.getAllColumns();
-    String incrementField =
-        introspectedTable.getTableConfiguration().getProperties().getProperty("incrementField");
-    if (incrementField != null) {
-      incrementField = incrementField.toUpperCase();
-    }
+    List<IntrospectedColumn> columns = insertColumns(introspectedTable);
 
     XmlElement insertBatchElement = new XmlElement("insert");
-    insertBatchElement.addAttribute(new Attribute("id", "batchInsert"));
-    insertBatchElement.addAttribute(new Attribute("parameterType", "java.util.List"));
-
-    StringBuilder sqlElement = new StringBuilder();
-    StringBuilder javaPropertyAndDbType = new StringBuilder("(");
-    for (IntrospectedColumn introspectedColumn : columns) {
-      String columnName = introspectedColumn.getActualColumnName();
-      if (!columnName.toUpperCase().equals(incrementField)) {
-        sqlElement.append(columnName + ",\n      ");
-        javaPropertyAndDbType.append(
-            "\n      #{item."
-                + introspectedColumn.getJavaProperty()
-                + ",jdbcType="
-                + introspectedColumn.getJdbcTypeName()
-                + "},");
-      }
-    }
+    insertBatchElement.addAttribute(new Attribute("id", "batchInsertRows"));
+    context.getCommentGenerator().addComment(insertBatchElement);
+    String columnNames =
+        columns.stream()
+            .map(MyBatis3FormattingUtilities::getEscapedColumnName)
+            .collect(Collectors.joining(", "));
+    String parameters =
+        columns.stream()
+            .map(column -> MyBatis3FormattingUtilities.getParameterClause(column, "item."))
+            .collect(Collectors.joining(", "));
 
     XmlElement foreachElement = new XmlElement("foreach");
     foreachElement.addAttribute(new Attribute("collection", "records"));
@@ -114,18 +169,11 @@ public class BatchInsertPlugin extends PluginAdapter {
     insertBatchElement.addElement(
         new TextElement(
             "insert into "
-                + introspectedTable.getAliasedFullyQualifiedTableNameAtRuntime()
-                + " ("));
-    insertBatchElement.addElement(
-        new TextElement(
-            "  " + sqlElement.delete(sqlElement.lastIndexOf(","), sqlElement.length()).toString()));
-    insertBatchElement.addElement(new TextElement(") values "));
-    foreachElement.addElement(
-        new TextElement(
-            javaPropertyAndDbType
-                .delete(javaPropertyAndDbType.length() - 1, javaPropertyAndDbType.length())
-                .append("\n      )")
-                .toString()));
+                + introspectedTable.getFullyQualifiedTableNameAtRuntime()
+                + " ("
+                + columnNames
+                + ") values"));
+    foreachElement.addElement(new TextElement("(" + parameters + ")"));
     insertBatchElement.addElement(foreachElement);
 
     document.getRootElement().addElement(insertBatchElement);
diff --git a/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/plugin/IoTDBKeyPlugin.java b/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/plugin/IoTDBKeyPlugin.java
new file mode 100644
index 0000000..2a1a0d6
--- /dev/null
+++ b/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/plugin/IoTDBKeyPlugin.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.iotdb.mybatis.plugin;
+
+import org.mybatis.generator.api.IntrospectedColumn;
+import org.mybatis.generator.api.IntrospectedTable;
+import org.mybatis.generator.api.PluginAdapter;
+import org.mybatis.generator.api.dom.xml.Attribute;
+import org.mybatis.generator.api.dom.xml.TextElement;
+import org.mybatis.generator.api.dom.xml.XmlElement;
+import org.mybatis.generator.codegen.mybatis3.MyBatis3FormattingUtilities;
+import org.mybatis.generator.config.TableConfiguration;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Adapts MBG's key-based statements to the IoTDB table model: nullable TAG components of the TIME +
+ * TAG key become {@code IS NULL} predicates in SELECT/DELETE, and the UPDATE statements are dropped
+ * because IoTDB UPDATE accepts neither {@code time} in the predicate nor FIELD columns in SET.
+ */
+public class IoTDBKeyPlugin extends PluginAdapter {
+  // MBG hands the shared warnings list to validate() before any table callback.
+  private List<String> warnings = new ArrayList<>();
+
+  @Override
+  public boolean validate(List<String> warnings) {
+    this.warnings = warnings;
+    return true;
+  }
+
+  @Override
+  public void initialized(IntrospectedTable table) {
+    TableConfiguration configuration = table.getTableConfiguration();
+    if (configuration.isUpdateByPrimaryKeyStatementEnabled()
+        || configuration.isUpdateByExampleStatementEnabled()) {
+      warnings.add(
+          "IoTDBKeyPlugin: not generating UPDATE statements for "
+              + table.getFullyQualifiedTable()
+              + "; IoTDB UPDATE cannot use time in the predicate or FIELD columns in SET. INSERT"
+              + " the same key to change FIELD values and set enableUpdateByPrimaryKey and"
+              + " enableUpdateByExample to false.");
+      configuration.setUpdateByPrimaryKeyStatementEnabled(false);
+      configuration.setUpdateByExampleStatementEnabled(false);
+    }
+  }
+
+  @Override
+  public boolean sqlMapSelectByPrimaryKeyElementGenerated(
+      XmlElement element, IntrospectedTable table) {
+    replaceKeyPredicate(element, table);
+    return true;
+  }
+
+  @Override
+  public boolean sqlMapDeleteByPrimaryKeyElementGenerated(
+      XmlElement element, IntrospectedTable table) {
+    replaceKeyPredicate(element, table);
+    return true;
+  }
+
+  private void replaceKeyPredicate(XmlElement element, IntrospectedTable table) {
+    // MBG emits the key WHERE clause as trailing text elements.
+    int where = -1;
+    for (int i = 0; i < element.getElements().size(); i++) {
+      if (element.getElements().get(i) instanceof TextElement
+          && ((TextElement) element.getElements().get(i))
+              .getContent()
+              .trim()
+              .startsWith("where ")) {
+        where = i;
+        break;
+      }
+    }
+    if (where < 0) return;
+    element.getElements().subList(where, element.getElements().size()).clear();
+    XmlElement predicate = new XmlElement("where");
+    for (IntrospectedColumn column : table.getPrimaryKeyColumns()) {
+      String name = MyBatis3FormattingUtilities.getEscapedColumnName(column);
+      String parameter = MyBatis3FormattingUtilities.getParameterClause(column);
+      if ("time".equalsIgnoreCase(column.getActualColumnName())) {
+        predicate.addElement(new TextElement("AND " + name + " = " + parameter));
+      } else {
+        XmlElement choose = new XmlElement("choose");
+        XmlElement when = new XmlElement("when");
+        when.addAttribute(new Attribute("test", column.getJavaProperty() + " != null"));
+        when.addElement(new TextElement("AND " + name + " = " + parameter));
+        XmlElement otherwise = new XmlElement("otherwise");
+        otherwise.addElement(new TextElement("AND " + name + " IS NULL"));
+        choose.addElement(when);
+        choose.addElement(otherwise);
+        predicate.addElement(choose);
+      }
+    }
+    element.addElement(predicate);
+  }
+}
diff --git a/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/plugin/LombokPlugin.java b/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/plugin/LombokPlugin.java
index 650d4ec..9e43443 100644
--- a/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/plugin/LombokPlugin.java
+++ b/mybatis-generator/src/main/java/org/apache/iotdb/mybatis/plugin/LombokPlugin.java
@@ -42,6 +42,18 @@ public class LombokPlugin extends PluginAdapter {
     return true;
   }
 
+  @Override
+  public boolean modelPrimaryKeyClassGenerated(
+      TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
+    return modelBaseRecordClassGenerated(topLevelClass, introspectedTable);
+  }
+
+  @Override
+  public boolean modelRecordWithBLOBsClassGenerated(
+      TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
+    return modelBaseRecordClassGenerated(topLevelClass, introspectedTable);
+  }
+
   @Override
   public boolean modelSetterMethodGenerated(
       Method method,
diff --git a/mybatis-generator/src/main/resources/generatorConfig.xml b/mybatis-generator/src/main/resources/generatorConfig.xml
index 78b9d73..a292630 100644
--- a/mybatis-generator/src/main/resources/generatorConfig.xml
+++ b/mybatis-generator/src/main/resources/generatorConfig.xml
@@ -18,7 +18,7 @@
 
 -->
 <generatorConfiguration>
-    <classPathEntry location="/apache/iotdb/iotdb-client/jdbc/target/iotdb-jdbc-2.0.3-jar-with-dependencies.jar"/>
+    <!-- The Maven generator plugin supplies iotdb-jdbc on its plugin classpath. -->
     <!-- mvn mybatis-generator:generate hierarchical/flat-->
     <context id="myBatis3Simple" targetRuntime="MyBatis3Simple" defaultModelType="flat">
         <!--        <property name="suppressAllComments" value="true"/>-->
@@ -29,6 +29,8 @@
     value="org.mybatis.generator.api.dom.DefaultXmlFormatter"/>-->
         <plugin type="org.apache.iotdb.mybatis.plugin.LombokPlugin"/>
         <plugin type="org.apache.iotdb.mybatis.plugin.BatchInsertPlugin"/>
+        <plugin type="org.apache.iotdb.mybatis.plugin.IoTDBKeyPlugin"/>
+        <plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin"/>
         <plugin type="org.apache.iotdb.mybatis.plugin.SerializablePlugin"/>
         <plugin type="org.mybatis.generator.plugins.VirtualPrimaryKeyPlugin"/>
         <!--        <commentGenerator type="generator.org.apache.iotdb.mybatis.plugin.CNCommentGenerator">-->
@@ -42,6 +44,7 @@
         <jdbcConnection driverClass="org.apache.iotdb.jdbc.IoTDBDriver" connectionURL="jdbc:iotdb://127.0.0.1:6667/test?sql_dialect=table" userId="root" password="root"/>
         <javaTypeResolver type="org.apache.iotdb.mybatis.plugin.generator.resolver.IoTDBJavaTypeResolver">
             <property name="forceBigDecimals" value="false"/>
+            <property name="jdbcType.FLOAT" value="java.lang.Float"/>
         </javaTypeResolver>
         <javaModelGenerator targetPackage="org.apache.iotdb.mybatis.plugin.model" targetProject="iotdb">
             <property name="enableSubPackages" value="true"/>
@@ -53,7 +56,7 @@
         <javaClientGenerator type="XMLMAPPER" targetPackage="org.apache.iotdb.mybatis.plugin.mapper" targetProject="iotdb">
             <property name="enableSubPackages" value="true"/>
         </javaClientGenerator>
-        <table schema="test" tableName="mix" domainObjectName="Mix" enableSelectByPrimaryKey="true" enableInsert="true" enableDeleteByPrimaryKey="true">
+        <table enableUpdateByPrimaryKey="false" enableUpdateByExample="false" schema="test" tableName="mix" domainObjectName="Mix" enableSelectByPrimaryKey="true" enableInsert="true" enableDeleteByPrimaryKey="true">
             <property name="virtualKeyColumns" value="time,device_id,device_name,device_tag"/>
         </table>
     </context>
diff --git a/mybatis-generator/src/test/java/org/apache/iotdb/mybatis/plugin/BatchInsertPluginTest.java b/mybatis-generator/src/test/java/org/apache/iotdb/mybatis/plugin/BatchInsertPluginTest.java
new file mode 100644
index 0000000..ff8350e
--- /dev/null
+++ b/mybatis-generator/src/test/java/org/apache/iotdb/mybatis/plugin/BatchInsertPluginTest.java
@@ -0,0 +1,274 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.iotdb.mybatis.plugin;
+
+import org.apache.ibatis.builder.xml.XMLMapperBuilder;
+import org.apache.ibatis.session.Configuration;
+import org.junit.Test;
+import org.mybatis.generator.api.IntrospectedColumn;
+import org.mybatis.generator.api.IntrospectedTable;
+import org.mybatis.generator.api.dom.DefaultJavaFormatter;
+import org.mybatis.generator.api.dom.DefaultXmlFormatter;
+import org.mybatis.generator.api.dom.java.FullyQualifiedJavaType;
+import org.mybatis.generator.api.dom.java.Interface;
+import org.mybatis.generator.api.dom.java.JavaVisibility;
+import org.mybatis.generator.api.dom.xml.Attribute;
+import org.mybatis.generator.api.dom.xml.Document;
+import org.mybatis.generator.api.dom.xml.XmlElement;
+import org.mybatis.generator.codegen.mybatis3.IntrospectedTableMyBatis3Impl;
+import org.mybatis.generator.config.Context;
+import org.mybatis.generator.config.ModelType;
+import org.mybatis.generator.config.TableConfiguration;
+import org.mybatis.generator.internal.rules.FlatModelRules;
+import org.mybatis.generator.internal.rules.HierarchicalModelRules;
+
+import javax.tools.ToolProvider;
+
+import java.io.ByteArrayInputStream;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Proxy;
+import java.net.URLClassLoader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.sql.Types;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+public class BatchInsertPluginTest {
+  private final Context context = new Context(ModelType.FLAT);
+  private final List<String> warnings = new ArrayList<>();
+  private final BatchInsertPlugin plugin = new BatchInsertPlugin();
+
+  private IntrospectedTable table() {
+    context.addProperty("beginningDelimiter", "\"");
+    context.addProperty("endingDelimiter", "\"");
+    plugin.setContext(context);
+    assertTrue(plugin.validate(warnings));
+    IntrospectedTable table =
+        new IntrospectedTableMyBatis3Impl() {
+          @Override
+          public String getFullyQualifiedTableNameAtRuntime() {
+            return "measurements";
+          }
+
+          @Override
+          public String getAliasedFullyQualifiedTableNameAtRuntime() {
+            return "measurements m";
+          }
+        };
+    TableConfiguration configuration = new TableConfiguration(context);
+    configuration.setTableName("measurements");
+    table.setTableConfiguration(configuration);
+    table.setContext(context);
+    table.setBaseRecordType("example.Measurement");
+    table.setRecordWithBLOBsType("example.MeasurementWithBLOBs");
+    table.setRules(new FlatModelRules(table));
+    column(table, "time", Types.TIMESTAMP, "TIMESTAMP", "java.lang.Long");
+    return table;
+  }
+
+  private IntrospectedColumn column(
+      IntrospectedTable table, String name, int type, String jdbc, String javaType) {
+    IntrospectedColumn column = new IntrospectedColumn();
+    column.setContext(context);
+    column.setActualColumnName(name);
+    column.setJavaProperty(name);
+    column.setJdbcType(type);
+    column.setJdbcTypeName(jdbc);
+    column.setFullyQualifiedJavaType(new FullyQualifiedJavaType(javaType));
+    table.addColumn(column);
+    return column;
+  }
+
+  private Interface mapper(IntrospectedTable table) {
+    Interface mapper = new Interface("example.MeasurementMapper");
+    mapper.setVisibility(JavaVisibility.PUBLIC);
+    plugin.clientGenerated(mapper, table);
+    return mapper;
+  }
+
+  private String xml(IntrospectedTable table) {
+    Document document =
+        new Document(
+            "-//mybatis.org//DTD Mapper 3.0//EN", "https://mybatis.org/dtd/mybatis-3-mapper.dtd");
+    XmlElement root = new XmlElement("mapper");
+    root.addAttribute(new Attribute("namespace", "example.MeasurementMapper"));
+    document.setRootElement(root);
+    plugin.sqlMapDocumentGenerated(document, table);
+    return new DefaultXmlFormatter().getFormattedContent(document);
+  }
+
+  @Test
+  public void generatedMapperCompilesAndGuardsAndSplitsBatches() throws Exception {
+    IntrospectedTable table = table();
+    Properties properties = new Properties();
+    properties.setProperty("batchSize", "2");
+    plugin.setProperties(properties);
+    assertTrue(plugin.validate(warnings));
+    Path dir = Files.createTempDirectory("iotdb-mbg-test-");
+    try {
+      Path source = Files.createDirectories(dir.resolve("example"));
+      Files.writeString(
+          source.resolve("Measurement.java"), "package example; public class Measurement {}");
+      Files.writeString(
+          source.resolve("MeasurementMapper.java"),
+          new DefaultJavaFormatter().getFormattedContent(mapper(table)));
+      assertEquals(
+          0,
+          ToolProvider.getSystemJavaCompiler()
+              .run(
+                  null,
+                  null,
+                  null,
+                  "-proc:none",
+                  "-classpath",
+                  System.getProperty("java.class.path"),
+                  "-d",
+                  dir.toString(),
+                  source.resolve("Measurement.java").toString(),
+                  source.resolve("MeasurementMapper.java").toString()));
+      try (URLClassLoader loader =
+          new URLClassLoader(
+              new java.net.URL[] {dir.toUri().toURL()}, getClass().getClassLoader())) {
+        Class<?> mapper = loader.loadClass("example.MeasurementMapper");
+        Object row = loader.loadClass("example.Measurement").getConstructor().newInstance();
+        List<Integer> chunks = new ArrayList<>();
+        java.util.concurrent.atomic.AtomicBoolean unknownCount =
+            new java.util.concurrent.atomic.AtomicBoolean();
+        Object instance =
+            Proxy.newProxyInstance(
+                loader,
+                new Class<?>[] {mapper},
+                (proxy, method, args) -> {
+                  if (method.isDefault())
+                    return InvocationHandler.invokeDefault(proxy, method, args);
+                  chunks.add(((List<?>) args[0]).size());
+                  return unknownCount.get() ? -1 : ((List<?>) args[0]).size();
+                });
+        java.lang.reflect.Method insert = mapper.getMethod("batchInsert", List.class);
+        assertEquals(0, insert.invoke(instance, List.of()));
+        assertTrue(chunks.isEmpty());
+        assertEquals(5, insert.invoke(instance, List.of(row, row, row, row, row)));
+        assertEquals(List.of(2, 2, 1), chunks);
+        unknownCount.set(true);
+        assertEquals(-1, insert.invoke(instance, List.of(row, row, row)));
+        chunks.clear();
+        InvocationTargetException failure =
+            assertThrows(
+                InvocationTargetException.class,
+                () -> insert.invoke(instance, Arrays.asList(row, row, row, null)));
+        assertTrue(failure.getCause() instanceof IllegalArgumentException);
+        assertTrue(chunks.isEmpty());
+        assertThrows(
+            InvocationTargetException.class, () -> insert.invoke(instance, new Object[] {null}));
+      }
+    } finally {
+      try (java.util.stream.Stream<Path> files = Files.walk(dir)) {
+        for (Path path :
+            files
+                .sorted(java.util.Comparator.reverseOrder())
+                .collect(java.util.stream.Collectors.toList())) {
+          Files.delete(path);
+        }
+      }
+    }
+  }
+
+  @Test
+  public void preservesEscapingHandlersAndParameterCounts() {
+    IntrospectedTable table = table();
+    IntrospectedColumn order = column(table, "order", Types.BIGINT, "BIGINT", "java.lang.Long");
+    order.setColumnNameDelimited(true);
+    order.setTypeHandler("org.apache.ibatis.type.LongTypeHandler");
+    String xml = xml(table);
+    assertTrue(xml.contains("\"order\""));
+    assertTrue(xml.contains("typeHandler=org.apache.ibatis.type.LongTypeHandler"));
+    assertFalse(xml.contains("measurements m"));
+    Configuration configuration = new Configuration();
+    new XMLMapperBuilder(
+            new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)),
+            configuration,
+            "mapper.xml",
+            configuration.getSqlFragments())
+        .parse();
+    org.apache.ibatis.mapping.BoundSql sql =
+        configuration
+            .getMappedStatement("example.MeasurementMapper.batchInsertRows")
+            .getBoundSql(
+                Map.of(
+                    "records",
+                    List.of(Map.of("time", 1L, "order", 2L), Map.of("time", 3L, "order", 4L))));
+    assertEquals(4, sql.getParameterMappings().size());
+    assertTrue(sql.getSql().replaceAll("\\s+", " ").contains("(?, ?)"));
+  }
+
+  @Test
+  public void excludesGeneratedColumnsAndSkipsTablesWithoutInsertableColumns() {
+    IntrospectedTable table = table();
+    column(table, "identity", Types.BIGINT, "BIGINT", "java.lang.Long").setIdentity(true);
+    column(table, "generated", Types.BIGINT, "BIGINT", "java.lang.Long").setGeneratedAlways(true);
+    column(table, "automatic", Types.BIGINT, "BIGINT", "java.lang.Long").setAutoIncrement(true);
+    assertFalse(xml(table).contains("identity"));
+    assertFalse(xml(table).contains("item.generated"));
+    assertFalse(xml(table).contains("item.automatic"));
+    table.getTableConfiguration().addProperty("incrementField", "TIME");
+    plugin.initialized(table);
+    assertTrue(warnings.get(0).contains("no insertable columns"));
+    assertTrue(mapper(table).getMethods().isEmpty());
+    assertFalse(xml(table).contains("<insert"));
+  }
+
+  @Test
+  public void usesTheAllFieldsTypeForHierarchicalBlobModels() {
+    IntrospectedTable table = table();
+    column(table, "payload", Types.BLOB, "BLOB", "byte[]");
+    table.setRules(new HierarchicalModelRules(table));
+    assertTrue(
+        mapper(table)
+            .getMethods()
+            .get(0)
+            .getParameters()
+            .get(0)
+            .getType()
+            .getFullyQualifiedName()
+            .contains("MeasurementWithBLOBs"));
+  }
+
+  @Test
+  public void honorsDisabledInsertAndRejectsInvalidBatchSizes() {
+    IntrospectedTable table = table();
+    table.getTableConfiguration().setInsertStatementEnabled(false);
+    assertTrue(mapper(table).getMethods().isEmpty());
+    for (String value : List.of("0", "-1", "not-a-number")) {
+      Properties properties = new Properties();
+      properties.setProperty("batchSize", value);
+      plugin.setProperties(properties);
+      assertFalse(plugin.validate(warnings));
+    }
+  }
+}
diff --git a/mybatis-generator/src/test/java/org/apache/iotdb/mybatis/plugin/IoTDBKeyPluginTest.java b/mybatis-generator/src/test/java/org/apache/iotdb/mybatis/plugin/IoTDBKeyPluginTest.java
new file mode 100644
index 0000000..0d6b01d
--- /dev/null
+++ b/mybatis-generator/src/test/java/org/apache/iotdb/mybatis/plugin/IoTDBKeyPluginTest.java
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.iotdb.mybatis.plugin;
+
+import org.apache.ibatis.builder.xml.XMLMapperBuilder;
+import org.apache.ibatis.mapping.BoundSql;
+import org.apache.ibatis.session.Configuration;
+import org.junit.Test;
+import org.mybatis.generator.api.IntrospectedColumn;
+import org.mybatis.generator.api.IntrospectedTable;
+import org.mybatis.generator.api.dom.DefaultXmlFormatter;
+import org.mybatis.generator.api.dom.java.FullyQualifiedJavaType;
+import org.mybatis.generator.api.dom.xml.Attribute;
+import org.mybatis.generator.api.dom.xml.Document;
+import org.mybatis.generator.api.dom.xml.TextElement;
+import org.mybatis.generator.api.dom.xml.XmlElement;
+import org.mybatis.generator.codegen.mybatis3.IntrospectedTableMyBatis3Impl;
+import org.mybatis.generator.config.Context;
+import org.mybatis.generator.config.ModelType;
+import org.mybatis.generator.config.TableConfiguration;
+import org.mybatis.generator.internal.rules.FlatModelRules;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.sql.Types;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class IoTDBKeyPluginTest {
+  private final Context context = new Context(ModelType.FLAT);
+  private final List<String> warnings = new ArrayList<>();
+  private final IoTDBKeyPlugin plugin = new IoTDBKeyPlugin();
+
+  private IntrospectedTable table() {
+    context.addProperty("beginningDelimiter", "\"");
+    context.addProperty("endingDelimiter", "\"");
+    plugin.setContext(context);
+    assertTrue(plugin.validate(warnings));
+    IntrospectedTable table =
+        new IntrospectedTableMyBatis3Impl() {
+          @Override
+          public String getFullyQualifiedTableNameAtRuntime() {
+            return "mix";
+          }
+        };
+    TableConfiguration configuration = new TableConfiguration(context);
+    configuration.setTableName("mix");
+    table.setTableConfiguration(configuration);
+    table.setContext(context);
+    table.setBaseRecordType("example.Mix");
+    table.setRules(new FlatModelRules(table));
+    column(table, "time", Types.TIMESTAMP, "TIMESTAMP", "java.lang.Long");
+    column(table, "device_id", Types.VARCHAR, "VARCHAR", "java.lang.String");
+    column(table, "temperature", Types.FLOAT, "FLOAT", "java.lang.Float");
+    table.addPrimaryKeyColumn("time");
+    table.addPrimaryKeyColumn("device_id");
+    return table;
+  }
+
+  private void column(
+      IntrospectedTable table, String name, int type, String jdbc, String javaType) {
+    IntrospectedColumn column = new IntrospectedColumn();
+    column.setContext(context);
+    column.setActualColumnName(name);
+    column.setJavaProperty(
+        name.equals("device_id") ? "deviceId" : name); // MBG camel-cases the property name
+    column.setColumnNameDelimited(true); // matches delimitAllColumns in the example configs
+    column.setJdbcType(type);
+    column.setJdbcTypeName(jdbc);
+    column.setFullyQualifiedJavaType(new FullyQualifiedJavaType(javaType));
+    table.addColumn(column);
+  }
+
+  /** Mirrors the trailing "where ... and ..." text elements MBG emits for the key predicate. */
+  private XmlElement keyedStatement(String tag, String id, String prefix) {
+    XmlElement element = new XmlElement(tag);
+    element.addAttribute(new Attribute("id", id));
+    if (tag.equals("select")) {
+      element.addAttribute(new Attribute("resultType", "map"));
+    }
+    element.addElement(new TextElement(prefix + " \"mix\""));
+    element.addElement(new TextElement("where \"time\" = #{time,jdbcType=TIMESTAMP}"));
+    element.addElement(new TextElement("  and \"device_id\" = #{deviceId,jdbcType=VARCHAR}"));
+    return element;
+  }
+
+  private Configuration parse(XmlElement... statements) {
+    Document document =
+        new Document(
+            "-//mybatis.org//DTD Mapper 3.0//EN", "https://mybatis.org/dtd/mybatis-3-mapper.dtd");
+    XmlElement root = new XmlElement("mapper");
+    root.addAttribute(new Attribute("namespace", "example.MixMapper"));
+    for (XmlElement statement : statements) {
+      root.addElement(statement);
+    }
+    document.setRootElement(root);
+    Configuration configuration = new Configuration();
+    new XMLMapperBuilder(
+            new ByteArrayInputStream(
+                new DefaultXmlFormatter()
+                    .getFormattedContent(document)
+                    .getBytes(StandardCharsets.UTF_8)),
+            configuration,
+            "mapper.xml",
+            configuration.getSqlFragments())
+        .parse();
+    return configuration;
+  }
+
+  @Test
+  public void rewritesKeyPredicatesSoNullTagsMatchWithIsNull() {
+    IntrospectedTable table = table();
+    XmlElement select = keyedStatement("select", "selectByPrimaryKey", "select * from");
+    XmlElement delete = keyedStatement("delete", "deleteByPrimaryKey", "delete from");
+    assertTrue(plugin.sqlMapSelectByPrimaryKeyElementGenerated(select, table));
+    assertTrue(plugin.sqlMapDeleteByPrimaryKeyElementGenerated(delete, table));
+    Configuration configuration = parse(select, delete);
+    for (String statement : new String[] {"selectByPrimaryKey", "deleteByPrimaryKey"}) {
+      Map<String, Object> key = new HashMap<>();
+      key.put("time", 1700000000123L);
+      key.put("deviceId", "d1");
+      BoundSql bound =
+          configuration.getMappedStatement("example.MixMapper." + statement).getBoundSql(key);
+      String sql = bound.getSql().replaceAll("\\s+", " ");
+      assertTrue(sql, sql.contains("WHERE \"time\" = ? AND \"device_id\" = ?"));
+      assertEquals(2, bound.getParameterMappings().size());
+      key.put("deviceId", null);
+      bound = configuration.getMappedStatement("example.MixMapper." + statement).getBoundSql(key);
+      sql = bound.getSql().replaceAll("\\s+", " ");
+      assertTrue(sql, sql.contains("WHERE \"time\" = ? AND \"device_id\" IS NULL"));
+      assertEquals(1, bound.getParameterMappings().size());
+    }
+  }
+
+  @Test
+  public void leavesStatementsWithoutAKeyPredicateAlone() {
+    IntrospectedTable table = table();
+    XmlElement select = new XmlElement("select");
+    select.addAttribute(new Attribute("id", "selectAll"));
+    select.addElement(new TextElement("select * from \"mix\""));
+    assertTrue(plugin.sqlMapSelectByPrimaryKeyElementGenerated(select, table));
+    assertEquals(1, select.getElements().size());
+  }
+
+  @Test
+  public void disablesUpdateStatementsWithAWarning() {
+    IntrospectedTable table = table();
+    TableConfiguration configuration = table.getTableConfiguration();
+    configuration.setUpdateByPrimaryKeyStatementEnabled(true);
+    configuration.setUpdateByExampleStatementEnabled(true);
+    plugin.initialized(table);
+    assertFalse(configuration.isUpdateByPrimaryKeyStatementEnabled());
+    assertFalse(configuration.isUpdateByExampleStatementEnabled());
+    assertFalse(table.getRules().generateUpdateByPrimaryKeyWithoutBLOBs());
+    assertFalse(table.getRules().generateUpdateByPrimaryKeySelective());
+    assertFalse(table.getRules().generateUpdateByExampleWithoutBLOBs());
+    assertEquals(1, warnings.size());
+    assertTrue(warnings.get(0), warnings.get(0).contains("not generating UPDATE statements"));
+
+    plugin.initialized(table);
+    assertEquals("already disabled tables do not warn again", 1, warnings.size());
+  }
+}
diff --git a/mybatis-support/README.md b/mybatis-support/README.md
new file mode 100644
index 0000000..ef7c576
--- /dev/null
+++ b/mybatis-support/README.md
@@ -0,0 +1,102 @@
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+-->
+
+# IoTDB MyBatis runtime support
+
+Small runtime adapters for MyBatis and MyBatis-Plus with **JDK 17+ and IoTDB JDBC 2.0.11**. MyBatis is a provided dependency; the application selects its MyBatis/Boot version.
+
+## Install and register
+
+From the repository root:
+
+```sh
+mvn -pl mybatis-support -am install
+```
+
+The `with-mybatis` distribution profile (`mvn package -Pwith-mybatis`) also ships this jar next to the generator plugin in `apache-iotdb-<version>-mybatis-generator-plugin-bin.zip`.
+
+Add this application dependency (it is separate from the build-time generator plugin):
+
+```xml
+<dependency>
+    <groupId>org.apache.iotdb</groupId>
+    <artifactId>mybatis-support</artifactId>
+    <version>2.0.4-SNAPSHOT</version>
+</dependency>
+```
+
+Register the query interceptor on the IoTDB MyBatis configuration:
+
+```xml
+<plugins>
+    <plugin interceptor="org.apache.iotdb.mybatis.IoTDBQueryInterceptor"/>
+</plugins>
+```
+
+For MyBatis-Plus, expose `new IoTDBQueryInterceptor()` as a Spring `@Bean`. The [example](../examples/mybatisplus-generator/src/main/java/org/apache/iotdb/config/IoTDBMybatisConfiguration.java) registers it. For applications with several databases, register it only on the IoTDB `SqlSessionFactory`.
+
+In JDBC 2.0.11, a table-model prepared SELECT executed with `execute()` returns true without populating `getResultSet()`. MyBatis normally uses these two calls and can return an empty result. The interceptor routes prepared queries and cursors through **one** `executeQuery()` call and exposes that result to MyBatis. It preserves statement/result ownership and propagates errors; it does not retry or re-execute the SQL.
+
+## Type mapping
+
+| IoTDB type | Java type | Read/write mapping |
+|---|---|---|
+| TIME / TIMESTAMP | `Long` | Built-in `LongTypeHandler`, raw server-precision ticks |
+| FLOAT | `Float` | Built-in `FloatTypeHandler` |
+| DATE | `LocalDate` | `IoTDBLocalDateTypeHandler` |
+| BLOB | `byte[]` | `IoTDBBlobTypeHandler` |
+
+DATE uses JDBC `setDate/getDate` and checks `wasNull()`. JDBC 2.0.11 does not implement the typed `getObject(..., LocalDate.class)` path and can return a non-null date object for SQL NULL.
+
+BLOB uses the supported `setBinaryStream(int, InputStream, int)` overload and `getBytes`. The 2.0.11 driver's `setBytes` treats bytes as text, and `setBlob` is unsupported. Arbitrary bytes, including zero and non-UTF-8 bytes, must survive unchanged.
+
+Configure generator column overrides:
+
+```xml
+<columnOverride column="reading_date" javaType="java.time.LocalDate"
+    typeHandler="org.apache.iotdb.mybatis.type.IoTDBLocalDateTypeHandler"/>
+<columnOverride column="payload" javaType="byte[]"
+    typeHandler="org.apache.iotdb.mybatis.type.IoTDBBlobTypeHandler"/>
+```
+
+The handler must appear in both parameter mappings and result mappings. In MyBatis-Plus use `@TableName(autoResultMap = true)` and `@TableField(typeHandler = ...)`; generated XML also includes explicit mappings. Package scanning is available through `mybatis-plus.type-handlers-package=org.apache.iotdb.mybatis.type`.
+
+## Time precision and write results
+
+A timestamp such as `1700000000123456789L` is interpreted in the server's configured precision. Use matching values for all TIME and TIMESTAMP columns:
+
+| Server precision | Example raw value |
+|---|---|
+| ms | `1700000000123L` |
+| us | `1700000000123456L` |
+| ns | `1700000000123456789L` |
+
+There is no automatic unit conversion. `java.util.Date` and the driver's `setTimestamp` path cannot preserve arbitrary microsecond/nanosecond ticks; do not substitute them for Long without a separately verified conversion policy.
+
+JDBC 2.0.11 does not report reliable affected-row counts through `getUpdateCount()`; MyBatis write methods can return **-1 (unknown)** after successful execution. Do not interpret `result > 0` as the success criterion. Failures raise exceptions. A multi-row insert, several chunks, or a Spring transaction annotation does not provide transactional rollback.
+
+## Tests
+
+```sh
+mvn -pl mybatis-support -am test
+```
+
+Unit tests cover byte preservation, dates/NULLs, raw ms/us/ns values, and single execution of prepared queries. The [MyBatis](../examples/mybatis-generator/README.md) and [MyBatis-Plus](../examples/mybatisplus-generator/README.md) examples provide opt-in real-server tests that generate, compile and execute mappers.
diff --git a/mybatis-support/pom.xml b/mybatis-support/pom.xml
new file mode 100644
index 0000000..41063f9
--- /dev/null
+++ b/mybatis-support/pom.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.iotdb</groupId>
+        <artifactId>iotdb-extras-parent</artifactId>
+        <version>2.0.4-SNAPSHOT</version>
+    </parent>
+    <artifactId>mybatis-support</artifactId>
+    <name>IoTDB Extras: MyBatis Runtime Support</name>
+    <dependencies>
+        <dependency>
+            <groupId>org.mybatis</groupId>
+            <artifactId>mybatis</artifactId>
+            <version>3.5.19</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/mybatis-support/src/main/java/org/apache/iotdb/mybatis/IoTDBQueryInterceptor.java b/mybatis-support/src/main/java/org/apache/iotdb/mybatis/IoTDBQueryInterceptor.java
new file mode 100644
index 0000000..f9724c3
--- /dev/null
+++ b/mybatis-support/src/main/java/org/apache/iotdb/mybatis/IoTDBQueryInterceptor.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.iotdb.mybatis;
+
+import org.apache.ibatis.executor.statement.StatementHandler;
+import org.apache.ibatis.plugin.Interceptor;
+import org.apache.ibatis.plugin.Intercepts;
+import org.apache.ibatis.plugin.Invocation;
+import org.apache.ibatis.plugin.Signature;
+import org.apache.ibatis.session.ResultHandler;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Proxy;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.Statement;
+
+/**
+ * Uses executeQuery for MyBatis prepared queries. IoTDB JDBC 2.0.11 table statements return true
+ * from execute() without assigning their ResultSet. Register on the IoTDB SqlSessionFactory.
+ */
+@Intercepts({
+  @Signature(
+      type = StatementHandler.class,
+      method = "query",
+      args = {Statement.class, ResultHandler.class}),
+  @Signature(
+      type = StatementHandler.class,
+      method = "queryCursor",
+      args = {Statement.class})
+})
+public class IoTDBQueryInterceptor implements Interceptor {
+  @Override
+  public Object intercept(Invocation invocation) throws Throwable {
+    Object statement = invocation.getArgs()[0];
+    if (!(statement instanceof PreparedStatement)) return invocation.proceed();
+    invocation.getArgs()[0] = queryStatement((PreparedStatement) statement);
+    try {
+      return invocation.proceed();
+    } finally {
+      invocation.getArgs()[0] = statement;
+    }
+  }
+
+  static PreparedStatement queryStatement(PreparedStatement delegate) {
+    return (PreparedStatement)
+        Proxy.newProxyInstance(
+            PreparedStatement.class.getClassLoader(),
+            new Class<?>[] {PreparedStatement.class},
+            new java.lang.reflect.InvocationHandler() {
+              private ResultSet resultSet;
+
+              @Override
+              public Object invoke(Object proxy, java.lang.reflect.Method method, Object[] args)
+                  throws Throwable {
+                if ("execute".equals(method.getName()) && (args == null || args.length == 0)) {
+                  resultSet = delegate.executeQuery();
+                  return resultSet != null;
+                }
+                if ("getResultSet".equals(method.getName())) return resultSet;
+                try {
+                  return method.invoke(delegate, args);
+                } catch (InvocationTargetException e) {
+                  throw e.getCause();
+                }
+              }
+            });
+  }
+}
diff --git a/mybatis-support/src/main/java/org/apache/iotdb/mybatis/type/IoTDBBlobTypeHandler.java b/mybatis-support/src/main/java/org/apache/iotdb/mybatis/type/IoTDBBlobTypeHandler.java
new file mode 100644
index 0000000..e787822
--- /dev/null
+++ b/mybatis-support/src/main/java/org/apache/iotdb/mybatis/type/IoTDBBlobTypeHandler.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.iotdb.mybatis.type;
+
+import org.apache.ibatis.type.BaseTypeHandler;
+import org.apache.ibatis.type.JdbcType;
+import org.apache.ibatis.type.MappedJdbcTypes;
+import org.apache.ibatis.type.MappedTypes;
+
+import java.io.ByteArrayInputStream;
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+@MappedTypes(byte[].class)
+@MappedJdbcTypes(
+    value = {JdbcType.BLOB, JdbcType.BINARY, JdbcType.VARBINARY},
+    includeNullJdbcType = true)
+public class IoTDBBlobTypeHandler extends BaseTypeHandler<byte[]> {
+  @Override
+  public void setNonNullParameter(
+      PreparedStatement statement, int index, byte[] value, JdbcType type) throws SQLException {
+    // IoTDB 2.0.11 encodes this overload as a hexadecimal BLOB literal. setBytes decodes text.
+    statement.setBinaryStream(index, new ByteArrayInputStream(value), value.length);
+  }
+
+  @Override
+  public byte[] getNullableResult(ResultSet resultSet, String column) throws SQLException {
+    return resultSet.getBytes(column);
+  }
+
+  @Override
+  public byte[] getNullableResult(ResultSet resultSet, int column) throws SQLException {
+    return resultSet.getBytes(column);
+  }
+
+  @Override
+  public byte[] getNullableResult(CallableStatement statement, int column) throws SQLException {
+    return statement.getBytes(column);
+  }
+}
diff --git a/mybatis-support/src/main/java/org/apache/iotdb/mybatis/type/IoTDBLocalDateTypeHandler.java b/mybatis-support/src/main/java/org/apache/iotdb/mybatis/type/IoTDBLocalDateTypeHandler.java
new file mode 100644
index 0000000..a516c66
--- /dev/null
+++ b/mybatis-support/src/main/java/org/apache/iotdb/mybatis/type/IoTDBLocalDateTypeHandler.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.iotdb.mybatis.type;
+
+import org.apache.ibatis.type.BaseTypeHandler;
+import org.apache.ibatis.type.JdbcType;
+import org.apache.ibatis.type.MappedJdbcTypes;
+import org.apache.ibatis.type.MappedTypes;
+
+import java.sql.CallableStatement;
+import java.sql.Date;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.time.LocalDate;
+
+@MappedTypes(LocalDate.class)
+@MappedJdbcTypes(value = JdbcType.DATE, includeNullJdbcType = true)
+public class IoTDBLocalDateTypeHandler extends BaseTypeHandler<LocalDate> {
+  @Override
+  public void setNonNullParameter(
+      PreparedStatement statement, int index, LocalDate value, JdbcType type) throws SQLException {
+    statement.setDate(index, Date.valueOf(value));
+  }
+
+  @Override
+  public LocalDate getNullableResult(ResultSet resultSet, String column) throws SQLException {
+    Date value = resultSet.getDate(column);
+    return resultSet.wasNull() ? null : toLocalDate(value);
+  }
+
+  @Override
+  public LocalDate getNullableResult(ResultSet resultSet, int column) throws SQLException {
+    Date value = resultSet.getDate(column);
+    return resultSet.wasNull() ? null : toLocalDate(value);
+  }
+
+  @Override
+  public LocalDate getNullableResult(CallableStatement statement, int column) throws SQLException {
+    Date value = statement.getDate(column);
+    return statement.wasNull() ? null : toLocalDate(value);
+  }
+
+  private LocalDate toLocalDate(Date value) {
+    return value == null ? null : value.toLocalDate();
+  }
+}
diff --git a/mybatis-support/src/test/java/org/apache/iotdb/mybatis/IoTDBQueryInterceptorTest.java b/mybatis-support/src/test/java/org/apache/iotdb/mybatis/IoTDBQueryInterceptorTest.java
new file mode 100644
index 0000000..7f12801
--- /dev/null
+++ b/mybatis-support/src/test/java/org/apache/iotdb/mybatis/IoTDBQueryInterceptorTest.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.iotdb.mybatis;
+
+import org.junit.Test;
+
+import java.lang.reflect.Proxy;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+public class IoTDBQueryInterceptorTest {
+  @Test
+  public void executesQueryOnceAndRetainsResultUntilMyBatisClosesStatement() throws Exception {
+    AtomicInteger queries = new AtomicInteger();
+    AtomicInteger closes = new AtomicInteger();
+    ResultSet result =
+        (ResultSet)
+            Proxy.newProxyInstance(
+                getClass().getClassLoader(), new Class<?>[] {ResultSet.class}, (p, m, a) -> null);
+    PreparedStatement driver =
+        (PreparedStatement)
+            Proxy.newProxyInstance(
+                getClass().getClassLoader(),
+                new Class<?>[] {PreparedStatement.class},
+                (p, m, a) -> {
+                  if ("executeQuery".equals(m.getName())) {
+                    queries.incrementAndGet();
+                    return result;
+                  }
+                  if ("close".equals(m.getName())) {
+                    closes.incrementAndGet();
+                    return null;
+                  }
+                  if ("getResultSet".equals(m.getName())) return null;
+                  throw new AssertionError("Unexpected JDBC call: " + m.getName());
+                });
+    PreparedStatement statement = IoTDBQueryInterceptor.queryStatement(driver);
+    assertTrue(statement.execute());
+    assertSame(result, statement.getResultSet());
+    assertSame(result, statement.getResultSet());
+    assertEquals(1, queries.get());
+    assertEquals(0, closes.get());
+    statement.close();
+    assertEquals(1, closes.get());
+  }
+
+  @Test
+  public void preservesSqlExceptionsWithoutRetrying() {
+    SQLException expected = new SQLException("query rejected");
+    AtomicInteger queries = new AtomicInteger();
+    PreparedStatement driver =
+        (PreparedStatement)
+            Proxy.newProxyInstance(
+                getClass().getClassLoader(),
+                new Class<?>[] {PreparedStatement.class},
+                (p, m, a) -> {
+                  queries.incrementAndGet();
+                  throw expected;
+                });
+    PreparedStatement statement = IoTDBQueryInterceptor.queryStatement(driver);
+    assertSame(expected, assertThrows(SQLException.class, statement::execute));
+    assertEquals(1, queries.get());
+  }
+}
diff --git a/mybatis-support/src/test/java/org/apache/iotdb/mybatis/type/IoTDBTypeHandlerTest.java b/mybatis-support/src/test/java/org/apache/iotdb/mybatis/type/IoTDBTypeHandlerTest.java
new file mode 100644
index 0000000..c6df49f
--- /dev/null
+++ b/mybatis-support/src/test/java/org/apache/iotdb/mybatis/type/IoTDBTypeHandlerTest.java
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.iotdb.mybatis.type;
+
+import org.apache.ibatis.type.JdbcType;
+import org.apache.ibatis.type.LongTypeHandler;
+import org.junit.Test;
+
+import java.io.InputStream;
+import java.lang.reflect.Proxy;
+import java.sql.Date;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.time.LocalDate;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+public class IoTDBTypeHandlerTest {
+  @Test
+  public void preservesBinaryBytesAndUsesTheSupportedStreamOverload() throws Exception {
+    byte[] bytes = {(byte) 0xff, 0, 39, (byte) 0x80};
+    AtomicReference<byte[]> bound = new AtomicReference<>();
+    PreparedStatement statement =
+        proxy(
+            PreparedStatement.class,
+            (method, args) -> {
+              assertEquals("setBinaryStream", method);
+              assertEquals(Integer.valueOf(bytes.length), args[2]);
+              bound.set(((InputStream) args[1]).readAllBytes());
+              return null;
+            });
+    IoTDBBlobTypeHandler handler = new IoTDBBlobTypeHandler();
+    handler.setParameter(statement, 1, bytes, JdbcType.BLOB);
+    assertArrayEquals(bytes, bound.get());
+    ResultSet results =
+        proxy(
+            ResultSet.class,
+            (method, args) -> {
+              assertEquals("getBytes", method);
+              return bytes;
+            });
+    assertArrayEquals(bytes, handler.getResult(results, "payload"));
+    assertArrayEquals(bytes, handler.getResult(results, 1));
+  }
+
+  @Test
+  public void usesJdbcDateWithoutJdbc42GetObject() throws Exception {
+    LocalDate value = LocalDate.of(2024, 2, 29);
+    PreparedStatement statement =
+        proxy(
+            PreparedStatement.class,
+            (method, args) -> {
+              assertEquals("setDate", method);
+              assertEquals(Date.valueOf(value), args[1]);
+              return null;
+            });
+    IoTDBLocalDateTypeHandler handler = new IoTDBLocalDateTypeHandler();
+    handler.setParameter(statement, 1, value, JdbcType.DATE);
+    ResultSet results =
+        proxy(
+            ResultSet.class,
+            (method, args) -> {
+              if (method.equals("wasNull")) return false;
+              assertEquals("getDate", method);
+              return Date.valueOf(value);
+            });
+    assertEquals(value, handler.getResult(results, "reading_date"));
+    assertEquals(value, handler.getResult(results, 1));
+  }
+
+  @Test
+  public void preservesNullDatesAndBlobs() throws Exception {
+    ResultSet results =
+        proxy(
+            ResultSet.class,
+            (method, args) -> {
+              if (method.equals("wasNull")) return true;
+              if (method.equals("getDate")) return Date.valueOf("0002-11-30");
+              return null;
+            });
+    assertNull(new IoTDBLocalDateTypeHandler().getResult(results, "reading_date"));
+    assertNull(new IoTDBBlobTypeHandler().getResult(results, "payload"));
+    for (JdbcType type : new JdbcType[] {JdbcType.DATE, JdbcType.BLOB}) {
+      AtomicReference<Integer> boundType = new AtomicReference<>();
+      PreparedStatement statement =
+          proxy(
+              PreparedStatement.class,
+              (method, args) -> {
+                assertEquals("setNull", method);
+                boundType.set((Integer) args[1]);
+                return null;
+              });
+      if (type == JdbcType.DATE) {
+        new IoTDBLocalDateTypeHandler().setParameter(statement, 1, null, type);
+      } else {
+        new IoTDBBlobTypeHandler().setParameter(statement, 1, null, type);
+      }
+      assertEquals(Integer.valueOf(type.TYPE_CODE), boundType.get());
+    }
+  }
+
+  @Test
+  public void longHandlerPreservesRawMillisecondsMicrosecondsAndNanoseconds() throws Exception {
+    for (long ticks : new long[] {1700000000123L, 1700000000123456L, 1700000000123456789L}) {
+      PreparedStatement statement =
+          proxy(
+              PreparedStatement.class,
+              (method, args) -> {
+                assertEquals("setLong", method);
+                assertEquals(ticks, args[1]);
+                return null;
+              });
+      ResultSet results =
+          proxy(
+              ResultSet.class,
+              (method, args) -> {
+                if (method.equals("wasNull")) return false;
+                assertEquals("getLong", method);
+                return ticks;
+              });
+      LongTypeHandler handler = new LongTypeHandler();
+      handler.setParameter(statement, 1, ticks, JdbcType.TIMESTAMP);
+      assertEquals(Long.valueOf(ticks), handler.getResult(results, "time"));
+    }
+  }
+
+  interface Call {
+    Object invoke(String method, Object[] args) throws Throwable;
+  }
+
+  private static <T> T proxy(Class<T> type, Call call) {
+    return type.cast(
+        Proxy.newProxyInstance(
+            type.getClassLoader(),
+            new Class<?>[] {type},
+            (proxy, method, args) -> call.invoke(method.getName(), args)));
+  }
+}
diff --git a/pom.xml b/pom.xml
index b155418..6704b40 100644
--- a/pom.xml
+++ b/pom.xml
@@ -38,32 +38,30 @@
         <module>iotdb-collector</module>
         <module>metric-scrape</module>
         <module>mybatis-generator</module>
+        <module>mybatis-support</module>
     </modules>
     <properties>
         <!-- Explicitly set a variable used by all dependencies to the IoTDB dependencies, as the
     release plugin will not update these on a release -->
-        <!-- This was the last version to support Java 8 -->
         <airlift-units.version>1.7</airlift-units.version>
         <airlift.version>206</airlift.version>
         <!--airlift-stats.version>235</airlift-stats.version-->
         <airline.version>0.9</airline.version>
-        <!-- This was the last version to support Java 8 -->
-        <antlr4.version>4.9.3</antlr4.version>
+        <antlr4.version>4.13.2</antlr4.version>
         <!-- By default, the argLine is empty-->
         <argLine/>
         <awaitility.version>4.2.0</awaitility.version>
         <boost.include.dir/>
-        <!-- This was the last version to support Java 8 -->
         <caffeine.version>2.9.3</caffeine.version>
         <cglib.version>3.3.0</cglib.version>
         <checker-qual.version>3.38.0</checker-qual.version>
         <cmake.build.type>Release</cmake.build.type>
         <commons-cli.version>1.5.0</commons-cli.version>
-        <commons-codec.version>1.16.0</commons-codec.version>
+        <commons-codec.version>1.16.1</commons-codec.version>
         <commons-csv.version>1.10.0</commons-csv.version>
         <commons-io.version>2.14.0</commons-io.version>
         <commons-jexl3.version>3.3</commons-jexl3.version>
-        <commons-lang3.version>3.13.0</commons-lang3.version>
+        <commons-lang3.version>3.18.0</commons-lang3.version>
         <commons-math3.version>3.6.1</commons-math3.version>
         <commons-pool2.version>2.11.1</commons-pool2.version>
         <commons.collections4.version>4.4</commons.collections4.version>
@@ -79,8 +77,8 @@
         <flink-shaded-zookeeper-3.version>3.8.1-17.0</flink-shaded-zookeeper-3.version>
         <flink.version>1.17.1</flink.version>
         <fusesource-mqtt-client.version>1.16</fusesource-mqtt-client.version>
-        <google.java.format.version>1.22.0</google.java.format.version>
-        <gson.version>2.10.1</gson.version>
+        <google.java.format.version>1.28.0</google.java.format.version>
+        <gson.version>2.13.1</gson.version>
         <guava.version>32.1.2-jre</guava.version>
         <hadoop.version>3.3.6</hadoop.version>
         <hive.version>3.1.3</hive.version>
@@ -90,9 +88,9 @@
       This is the version of the thrift binary, that we release separately from here:
       https://github.com/apache/iotdb-bin-resources/tree/main/iotdb-tools-thrift
     -->
-        <iotdb-tools-thrift.version>0.14.1.0</iotdb-tools-thrift.version>
-        <iotdb.version>2.0.5</iotdb.version>
-        <jackson.version>2.16.2</jackson.version>
+        <iotdb-tools-thrift.version>0.23.0.0</iotdb-tools-thrift.version>
+        <iotdb.version>2.0.11</iotdb.version>
+        <jackson.version>2.18.6</jackson.version>
         <!-- This is the last version to support the javax namespace -->
         <jakarta.servlet-api.version>4.0.4</jakarta.servlet-api.version>
         <!-- This is the last version to support the javax namespace -->
@@ -103,48 +101,39 @@
         <jcip-annotations.version>1.0-1</jcip-annotations.version>
         <!-- This is the last version to support the javax namespace -->
         <jersey.version>2.40</jersey.version>
-        <!-- This was the last version to support Java 8 -->
         <jetty.version>9.4.57.v20241219</jetty.version>
         <jjwt.version>0.11.5</jjwt.version>
-        <jline.version>3.23.0</jline.version>
+        <jline.version>3.26.2</jline.version>
         <jna.version>5.14.0</jna.version>
         <json-smart.version>2.5.0</json-smart.version>
         <jtransforms.version>3.1</jtransforms.version>
         <junit.version>4.13.2</junit.version>
         <kafka.version>2.8.2</kafka.version>
-        <!-- This was the last version to support Java 8 -->
-        <logback.version>1.3.15</logback.version>
-        <lz4-java.version>1.8.0</lz4-java.version>
+        <logback.version>1.5.34</logback.version>
+        <lz4-java.version>1.10.1</lz4-java.version>
         <maven.assembly.version>3.6.0</maven.assembly.version>
-        <maven.compiler.source>1.8</maven.compiler.source>
-        <maven.compiler.target>1.8</maven.compiler.target>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+        <maven.compiler.release>17</maven.compiler.release>
         <micrometer.version>1.11.4</micrometer.version>
-        <milo.version>0.6.11</milo.version>
+        <milo.version>0.6.14</milo.version>
         <!-- It seems that powermock is having issues with the newest mockito versions -->
         <mockito.version>2.23.4</mockito.version>
-        <!-- This was the last version to support Java 8 -->
         <!--mockito.version>4.11.0</mockito.version-->
-        <moquette.version>0.17</moquette.version>
-        <netty.version>4.1.97.Final</netty.version>
+        <moquette.version>0.18.0</moquette.version>
+        <netty.version>4.1.134.Final</netty.version>
         <nimbus-jose-jwt.version>9.37.2</nimbus-jose-jwt.version>
         <oauth2-oidc-sdk.version>10.15</oauth2-oidc-sdk.version>
-        <!-- This was the last version to support Java 8 -->
-        <openapi.generator.version>6.6.0</openapi.generator.version>
+        <openapi.generator.version>7.10.0</openapi.generator.version>
         <orc-core.version>1.9.1</orc-core.version>
         <osgi.version>7.0.0</osgi.version>
         <pax-jdbc-common.version>1.5.6</pax-jdbc-common.version>
         <powermock.version>2.0.9</powermock.version>
-        <ratis-thirdparty-misc.version>1.0.5</ratis-thirdparty-misc.version>
-        <!--
-      This is an unreleased version of a custom branch. The 8-character part after the version number
-      This is an unreleased version of a custom branch. The 8-character part after the version number
-      is for ensuring the SNAPSHOT will stay available. We should however have the Ratis folks do a
-      new release soon, as releasing with this version is more than sub-ideal.
-    -->
-        <ratis.version>3.1.0-a6a21d5-SNAPSHOT</ratis.version>
+        <ratis-thirdparty-misc.version>1.1.0</ratis-thirdparty-misc.version>
+        <ratis.version>3.3.0</ratis.version>
         <reactive-streams.version>1.0.4</reactive-streams.version>
-        <reactor-netty.version>1.1.13</reactor-netty.version>
-        <reactor.version>3.5.10</reactor.version>
+        <reactor-netty.version>1.2.9</reactor-netty.version>
+        <reactor.version>3.7.9</reactor.version>
         <reflections.version>0.10.2</reflections.version>
         <rocketmq-client.version>5.1.3</rocketmq-client.version>
         <scala.library.version>2.12.19</scala.library.version>
@@ -152,7 +141,7 @@
         <!-- Newer version requires refactoring of testsuite -->
         <scalatest.version>3.0.9</scalatest.version>
         <slf4j.version>2.0.9</slf4j.version>
-        <snappy-java.version>1.1.10.4</snappy-java.version>
+        <snappy-java.version>1.1.10.5</snappy-java.version>
         <sonar.coverage.jacoco.xmlReportPaths>target/jacoco-merged-reports/jacoco.xml</sonar.coverage.jacoco.xmlReportPaths>
         <!-- Exclude all generated code -->
         <sonar.exclusions>**/generated-sources</sonar.exclusions>
@@ -165,29 +154,22 @@
         <spark-scala.2.12.version>3.5.0</spark-scala.2.12.version>
         <!-- Override this to `true`, if you want to disable spotless -->
         <spotless.skip>false</spotless.skip>
-        <spotless.version>2.43.0</spotless.version>
-        <!-- This is the matching version of spring-boot for spring 5.3.30 -->
+        <spotless.version>2.44.5</spotless.version>
+        <!-- Legacy Grafana WAR uses Spring Boot 2 / javax; the starter overrides these. -->
         <spring-boot.version>2.7.18</spring-boot.version>
         <!-- This is the last version to support the javax namespace -->
         <spring.version>5.3.39</spring.version>
         <sqlite.version>3.49.1.0</sqlite.version>
-        <!-- This was the last version to support Java 8 -->
         <swagger.version>1.6.14</swagger.version>
         <thrift.exec-cmd.executable>chmod</thrift.exec-cmd.executable>
         <thrift.exec.absolute.path/>
-        <!--
-      Thrift 0.17.0 was the last version that could be used in Java 8 applications,
-      However Thrift 0.17.0 has an invalid entry in the META-INF/MANIFEST.mf file.
-      All versions between 0.17.0 and 0.14.1 have know vulnerabilities, so for now
-      we'll stay at 0.14.1.
-    -->
-        <thrift.version>0.14.1</thrift.version>
-        <!-- This was the last version to support Java 8 -->
+        <!-- Match the runtime to the generated IoTDB 2.0.11 Thrift classes. -->
+        <thrift.version>0.23.0</thrift.version>
         <tomcat.version>9.0.86</tomcat.version>
-        <tsfile.version>2.1.1</tsfile.version>
+        <tsfile.version>2.4.0</tsfile.version>
         <xz.version>1.9</xz.version>
         <zeppelin.version>0.11.1</zeppelin.version>
-        <zstd-jni.version>1.5.5-5</zstd-jni.version>
+        <zstd-jni.version>1.5.6-3</zstd-jni.version>
     </properties>
     <!--
     if we claim dependencies in dependencyManagement, then we do not claim
@@ -196,6 +178,46 @@
   -->
     <dependencyManagement>
         <dependencies>
+            <!-- Match the released IoTDB v2.0.11 dependency baseline. -->
+            <dependency>
+                <groupId>io.netty</groupId>
+                <artifactId>netty-bom</artifactId>
+                <version>${netty.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <dependency>
+                <groupId>com.fasterxml.jackson</groupId>
+                <artifactId>jackson-bom</artifactId>
+                <version>${jackson.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <dependency>
+                <groupId>at.yawk.lz4</groupId>
+                <artifactId>lz4-java</artifactId>
+                <version>${lz4-java.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.xerial.snappy</groupId>
+                <artifactId>snappy-java</artifactId>
+                <version>${snappy-java.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.iotdb</groupId>
+                <artifactId>iotdb-jdbc</artifactId>
+                <version>${iotdb.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.iotdb</groupId>
+                <artifactId>iotdb-subscription</artifactId>
+                <version>${iotdb.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.iotdb</groupId>
+                <artifactId>isession</artifactId>
+                <version>${iotdb.version}</version>
+            </dependency>
             <dependency>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot</artifactId>
@@ -251,15 +273,38 @@
                 <artifactId>libthrift</artifactId>
                 <version>${thrift.version}</version>
                 <exclusions>
-                    <!-- Seemed suspicious to have this dependency in -->
+                    <!-- We don't use thrift HTTP client or servlet transports. -->
+                    <exclusion>
+                        <groupId>org.apache.httpcomponents.client5</groupId>
+                        <artifactId>httpclient5</artifactId>
+                    </exclusion>
                     <exclusion>
-                        <groupId>org.apache.tomcat.embed</groupId>
-                        <artifactId>tomcat-embed-core</artifactId>
+                        <groupId>org.apache.httpcomponents.core5</groupId>
+                        <artifactId>httpcore5</artifactId>
                     </exclusion>
-                    <!-- We don't want the old javax packages -->
                     <exclusion>
-                        <groupId>javax.annotation</groupId>
-                        <artifactId>javax.annotation-api</artifactId>
+                        <groupId>org.apache.httpcomponents.core5</groupId>
+                        <artifactId>httpcore5-h2</artifactId>
+                    </exclusion>
+                    <exclusion>
+                        <groupId>jakarta.servlet</groupId>
+                        <artifactId>jakarta.servlet-api</artifactId>
+                    </exclusion>
+                    <!--
+                      Thrift generated Java sources suppress @Generated annotations.
+                      IoTDB does not use libthrift APIs that require Jakarta annotations.
+                    -->
+                    <exclusion>
+                        <groupId>jakarta.annotation</groupId>
+                        <artifactId>jakarta.annotation-api</artifactId>
+                    </exclusion>
+                    <!--
+                      commons-lang3 is only used by Thrift's partial deserialization runtime.
+                      IoTDB does not use org.apache.thrift.partial APIs.
+                    -->
+                    <exclusion>
+                        <groupId>org.apache.commons</groupId>
+                        <artifactId>commons-lang3</artifactId>
                     </exclusion>
                 </exclusions>
             </dependency>
@@ -1100,7 +1145,7 @@
             <dependency>
                 <groupId>com.fasterxml.jackson.module</groupId>
                 <artifactId>jackson-module-jaxb-annotations</artifactId>
-                <version>2.15.2</version>
+                <version>${jackson.version}</version>
             </dependency>
             <!- - Conflict:
         hadoop-common (pulls in 1.9 and 1.10.0)
@@ -1669,7 +1714,7 @@
             </plugin>
             <!--
         Strange things usually happen if you run with a too low Java version.
-        This plugin not only checks the minimum java version of 1.8, but also
+        This plugin not only checks the minimum Java version of 17, but also
         checks all dependencies (and transitive dependencies) for reported CVEs.
       -->
             <plugin>
@@ -1693,6 +1738,23 @@
                     </dependency>
                 </dependencies>
                 <executions>
+                    <!-- Check Java independently of the optional dependency audit. -->
+                    <execution>
+                        <id>enforce-java-baseline</id>
+                        <goals>
+                            <goal>enforce</goal>
+                        </goals>
+                        <phase>validate</phase>
+                        <configuration>
+                            <skip>false</skip>
+                            <rules>
+                                <requireJavaVersion>
+                                    <version>[17,)</version>
+                                    <message>IoTDB Extras with IoTDB 2.0.11 requires JDK 17 or newer.</message>
+                                </requireJavaVersion>
+                            </rules>
+                        </configuration>
+                    </execution>
                     <!-- Ensure we're not mixing dependency versions -->
                     <execution>
                         <id>enforce-version-convergence</id>
@@ -1741,7 +1803,7 @@
                             <fail>true</fail>
                             <rules>
                                 <requireJavaVersion>
-                                    <version>1.8.0</version>
+                                    <version>[17,)</version>
                                 </requireJavaVersion>
                                 <banVulnerable implementation="org.sonatype.ossindex.maven.enforcer.BanVulnerableDependencies">
                                     <excludeCoordinates>
@@ -2025,43 +2087,8 @@
         </repository>
     </repositories-->
     <profiles>
-        <profile>
-            <id>.java-9-and-above</id>
-            <activation>
-                <jdk>[9,)</jdk>
-            </activation>
-            <properties>
-                <maven.compiler.release>8</maven.compiler.release>
-            </properties>
-        </profile>
-        <!-- Current version of spotless cannot support JDK11 below -->
-        <profile>
-            <id>.java-11-below</id>
-            <activation>
-                <jdk>(,11]</jdk>
-            </activation>
-            <properties>
-                <!-- This was the last version to support Java 8, Just for run -->
-                <spotless.version>2.27.1</spotless.version>
-                <!-- To avoid format conflicts -->
-                <spotless.skip>true</spotless.skip>
-            </properties>
-        </profile>
-        <!--
-      Add argLine for Java 16 and above, due to [JEP 396: Strongly Encapsulate JDK Internals by Default]
-      (https://openjdk.java.net/jeps/396)
-    -->
-        <profile>
-            <id>.java-16</id>
-            <activation>
-                <jdk>16</jdk>
-            </activation>
-            <properties>
-                <argLine>--illegal-access=permit --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.java [...]
-            </properties>
-        </profile>
         <!--
-      Add argLine for Java 16 and above, due to [JEP 396: Strongly Encapsulate JDK Internals by Default]
+      Add argLine for Java 17 and above, due to [JEP 396: Strongly Encapsulate JDK Internals by Default]
       (https://openjdk.java.net/jeps/396)
     -->
         <profile>
@@ -2079,8 +2106,7 @@
       opt-in convention (cf. the with-springboot profile / iotdb-spring-boot-starter), it is wired
       as an explicit, named profile rather than JDK-auto-activated, so a plain `mvn clean verify`
       never pulls it into the reactor implicitly. CI builds and tests it on the JDK 17+ jobs by
-      passing `-P with-thingsboard` (see .github/workflows/compile-check.yml); the JDK 8/11 jobs
-      omit the flag and skip it.
+      passing `-P with-thingsboard` (see .github/workflows/compile-check.yml).
     -->
         <profile>
             <id>with-thingsboard</id>