diff --git a/.github/workflows/compile-check.yml b/.github/workflows/compile-check.yml index 801631c5..71ca6593 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 f9dd7764..da14cc49 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 ffe425f8..36160756 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 270ead1a..3cc78310 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 7895abc5..d332d8f1 100644 --- a/connectors/flink-iotdb-connector/pom.xml +++ b/connectors/flink-iotdb-connector/pom.xml @@ -25,7 +25,6 @@ flink-iotdb-connector IoTDB: Connector: Apache Flink - 1.8 UTF-8 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 535ff744..97591d28 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 static void setGlobalTSFileConfig(TSFileConfig config) { 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 0050e0ed..2621ac3c 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 void testTSFileConfigUtilCompleteness() { "setDoubleCompression", "setInt32Compression", "setFloatCompression", - "setEncryptKeyFromToken" + "setEncryptKeyFromToken", + "setEncryptSalt" }; Set addedSetters = new HashSet<>(); Collections.addAll(addedSetters, setters); diff --git a/connectors/grafana-connector/pom.xml b/connectors/grafana-connector/pom.xml index caa6e4c8..57a44fcf 100644 --- a/connectors/grafana-connector/pom.xml +++ b/connectors/grafana-connector/pom.xml @@ -37,6 +37,12 @@ org.apache.iotdb.web.grafana.TsfileWebDemoApplication + + + org.apache.iotdb + iotdb-jdbc + runtime + org.apache.tsfile tsfile @@ -209,6 +215,16 @@ + + org.apache.maven.plugins + maven-dependency-plugin + + + + org.apache.iotdb:iotdb-jdbc + + + org.springframework.boot spring-boot-maven-plugin diff --git a/connectors/grafana-plugin/docker-compose.yaml b/connectors/grafana-plugin/docker-compose.yaml index 597b4a88..32517008 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 69935473..43f8bf4d 100644 --- a/connectors/hive-connector/pom.xml +++ b/connectors/hive-connector/pom.xml @@ -29,7 +29,6 @@ hive-connector IoTDB: Connector: Apache Hive - 1.8 UTF-8 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 ea081274..2d810b6c 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 @@ private ObjectInspector createObjectInspectorWorker(TypeInfo ti) throws TsFileSe 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 f59ee4fa..4adada43 100644 --- a/connectors/spark-iotdb-connector/pom.xml +++ b/connectors/spark-iotdb-connector/pom.xml @@ -38,8 +38,8 @@ scala_2.12 - 11 - 11 + 17 + 17 UTF-8 diff --git a/connectors/spark-iotdb-table-connector/pom.xml b/connectors/spark-iotdb-table-connector/pom.xml index 232c9c4b..d2ce80dd 100644 --- a/connectors/spark-iotdb-table-connector/pom.xml +++ b/connectors/spark-iotdb-table-connector/pom.xml @@ -40,8 +40,8 @@ iotdb-table-connector-3.3 - 11 - 11 + 17 + 17 UTF-8 3.5.0 diff --git a/distributions/pom.xml b/distributions/pom.xml index cc002aa1..ae4f4f0f 100644 --- a/distributions/pom.xml +++ b/distributions/pom.xml @@ -119,6 +119,76 @@ + + with-mybatis + + + + org.apache.iotdb + mybatis-generator-plugin + ${project.version} + + + org.apache.iotdb + mybatis-support + ${project.version} + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + all-bin + + single + + package + + + src/assembly/mybatis-generator-plugin.xml + + apache-iotdb-${project.version} + + + + + + + net.nicoulaj.maven.plugins + checksum-maven-plugin + + + sign-source-release + + files + + package + + + SHA-512 + + + + + ${project.build.directory} + + apache-iotdb-${project.version}-mybatis-generator-plugin-bin.zip + + + + + + + + + + with-flink diff --git a/distributions/src/assembly/mybatis-generator-plugin.xml b/distributions/src/assembly/mybatis-generator-plugin.xml index 271c0844..dc00b42d 100644 --- a/distributions/src/assembly/mybatis-generator-plugin.xml +++ b/distributions/src/assembly/mybatis-generator-plugin.xml @@ -34,6 +34,14 @@ mybatis-generator-plugin-*.jar + + + ${maven.multiModuleProjectDirectory}/mybatis-support/target/ + ${file.separator} + + mybatis-support-*.jar + + common-files.xml diff --git a/examples/iotdb-spring-boot-start/README.md b/examples/iotdb-spring-boot-start/README.md index b9653f64..d0c5104a 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 - - ``` - - - org.springframework.boot - spring-boot-starter - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.apache.iotdb - iotdb-spring-boot-starter - 2.0.3 - - - ``` - -### 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 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 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 4fe181ca..8de45bf0 100644 --- a/examples/iotdb-spring-boot-start/pom.xml +++ b/examples/iotdb-spring-boot-start/pom.xml @@ -30,15 +30,15 @@ org.apache.iotdb iotdb-spring-boot-start-example - 2.0.4-SNAPHOT + 2.0.4-SNAPSHOT IoTDB: Example: SpringBoot Starter iotdb-spring-boot-start 17 3.5.1 - 1.22.0 - 2.43.0 - 2.0.5 + 1.28.0 + 2.44.5 + 2.0.11 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 f749e885..3c2247a4 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 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 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 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 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 0680ea7c..5185184c 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 765a0cba..7269d246 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. org.apache.iotdb iotdb-session - 2.0.5 + 2.0.11 ``` @@ -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 234f0a6b..3b1a6ad0 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 - - - - org.mybatis.generator - mybatis-generator-maven-plugin - 1.4.2 - - - org.apache.iotdb - mybatis-generator-plugin - 1.3.2 - - - - true - true - src/main/resources/generatorConfig.xml - - - - +```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 0a69db51..0828c797 100644 --- a/examples/mybatis-generator/pom.xml +++ b/examples/mybatis-generator/pom.xml @@ -29,8 +29,33 @@ mybatis-generator-example IoTDB: Example: Mybatis Generator - 2.0.4-SNAPHOT + 2.0.4-SNAPSHOT + + src/main/resources/generatorConfig.xml + + + org.apache.iotdb + mybatis-generator-plugin + 2.0.4-SNAPSHOT + test + + + org.mybatis.generator + mybatis-generator-core + 1.4.2 + test + + + junit + junit + test + + + org.apache.iotdb + mybatis-support + 2.0.4-SNAPSHOT + org.mybatis mybatis @@ -69,11 +94,26 @@ + + org.apache.maven.plugins + maven-surefire-plugin + + + integration-tests + none + + + org.mybatis.generator mybatis-generator-maven-plugin 1.4.2 + + org.apache.iotdb + iotdb-jdbc + ${iotdb.version} + org.apache.iotdb mybatis-generator-plugin @@ -83,7 +123,7 @@ true true - src/main/resources/generatorConfig.xml + ${mybatis.generator.configurationFile} @@ -94,6 +134,10 @@ org.apache.iotdb:iotdb-jdbc + + + org.apache.iotdb:mybatis-support + @@ -105,4 +149,26 @@ + + + iotdb-mybatis-it + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.5.3 + + + + integration-test + verify + + + + + + + + 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 5fb32d64..e08c7dba 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 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 static void main(String[] args) throws IOException { // 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 @@ private static void insertOne(MixMapper mapper, Date now) { 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 @@ private static void selectAll(MixMapper mapper) { 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 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 fb54c92d..7537b834 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.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 selectAll(); - int batchInsert(@Param("records") List records); + default int batchInsert(List 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 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 d32e3e45..b6e83c67 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 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 ea23c977..a245954a 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 @@ - + - - - - - - - - - - + + + + + + + + + + + + - delete from test.mix where time = #{time,jdbcType=TIMESTAMP} and device_id = #{deviceId,jdbcType=VARCHAR} - 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,jdbcType=TIMESTAMP}) - - - insert into test.mix ( time, device_id, region, plant_id, model_id, maintenance, temperature, humidity, status, arrival_time ) values - ( #{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} ) + "time" = #{time,jdbcType=TIMESTAMP} + + AND "device_id" = #{deviceId,jdbcType=VARCHAR} + AND "device_id" IS NULL + + + DELETE FROM "mix" WHERE + + + 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=TIMESTAMP}, #{readingDate,jdbcType=DATE,typeHandler=org.apache.iotdb.mybatis.type.IoTDBLocalDateTypeHandler}, #{payload,jdbcType=BLOB,typeHandler=org.apache.iotdb.mybatis.type.IoTDBBlobTypeHandler}) + + + INSERT INTO "mix" ("time", "region", "plant_id", "device_id", "model_id", "maintenance", "temperature", "humidity", "status", "arrival_time", "reading_date", "payload") VALUES + (#{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.IoTDBLocalDateTypeHandler}, #{item.payload,jdbcType=BLOB,typeHandler=org.apache.iotdb.mybatis.type.IoTDBBlobTypeHandler}) diff --git a/examples/mybatis-generator/src/main/resources/generatorConfig.xml b/examples/mybatis-generator/src/main/resources/generatorConfig.xml index 6ad1c100..6fe89535 100644 --- a/examples/mybatis-generator/src/main/resources/generatorConfig.xml +++ b/examples/mybatis-generator/src/main/resources/generatorConfig.xml @@ -18,7 +18,7 @@ --> - + @@ -27,6 +27,8 @@ + + @@ -40,6 +42,8 @@ + + @@ -51,8 +55,11 @@ - - +
+ + + +
diff --git a/examples/mybatis-generator/src/main/resources/generatorConfigByExample.xml b/examples/mybatis-generator/src/main/resources/generatorConfigByExample.xml index 7170ceb4..253041ec 100644 --- a/examples/mybatis-generator/src/main/resources/generatorConfigByExample.xml +++ b/examples/mybatis-generator/src/main/resources/generatorConfigByExample.xml @@ -18,9 +18,9 @@ --> - + - + + + @@ -42,6 +44,8 @@ + + @@ -53,8 +57,11 @@ - - +
+ + + +
diff --git a/examples/mybatis-generator/src/main/resources/mybatis-config.xml b/examples/mybatis-generator/src/main/resources/mybatis-config.xml index 06dfbe99..f8757c48 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 @@ --> + + + 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 00000000..303d864a --- /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 warnings = new ArrayList<>(); + new MyBatisGenerator(configuration, new DefaultShellCallback(true), warnings).generate(null); + assertTrue(warnings.toString(), warnings.isEmpty()); + + Path classes = output.newFolder("classes").toPath(); + List args = + new ArrayList<>( + List.of( + "-proc:none", + "-classpath", + System.getProperty("java.class.path"), + "-d", + classes.toString())); + try (java.util.stream.Stream 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 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 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 00000000..f7e0e9a2 --- /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 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 4314992a..a6947948 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 2.4.1 is declared explicitly for code generation. -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 -``` - - 3.5.10 - 17 - 17 - UTF-8 - 3.4.5 - 6.2.6 - 2.0.5 - 3.0.0 - - - - - com.baomidou - mybatis-plus-spring-boot3-starter - ${mybatisplus.version} - - - com.baomidou - mybatis-plus-generator - ${mybatisplus.version} - - - - org.apache.iotdb - iotdb-jdbc - ${iotdb-jdbc.version} - - - org.springframework.boot - spring-boot-starter - ${spring-boot.version} - - - org.springframework.boot - spring-boot-starter-web - ${spring-boot.version} - - - org.springframework.boot - spring-boot-starter-test - ${spring-boot.version} - test - - - - io.springfox - springfox-swagger2 - ${io-springfox.version} - - - io.springfox - springfox-swagger-ui - ${io-springfox.version} - - - - org.projectlombok - lombok - 1.18.36 - - - com.github.jeffreyning - mybatisplus-plus - 1.7.5-RELEASE - - -``` +TIME plus **all TAG columns** identifies a row. The example uses `IoTDBTableMapper` 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 so can delete other rows for the same device. diff --git a/examples/mybatisplus-generator/pom.xml b/examples/mybatisplus-generator/pom.xml index cf2b14cd..26bb891a 100644 --- a/examples/mybatisplus-generator/pom.xml +++ b/examples/mybatisplus-generator/pom.xml @@ -22,31 +22,67 @@ 4.0.0 - org.springframework.boot - spring-boot-starter-parent - 3.5.1 - - + org.apache.iotdb + examples + 2.0.4-SNAPSHOT - org.apache.iotdb mybatisplus-generator-example IoTDB: Example: Mybatis Plus Generator 2.0.4-SNAPSHOT - 3.5.10 - 17 - 17 + 3.5.15 + mybatis-plus-spring-boot3-starter + spring-boot-starter-web UTF-8 + 3.5.1 - 2.0.5 - 3.0.0 + 6.2.8 + 2.19.1 + 2.19.1 + 5.17.0 + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson-annotations.version} + + + + + org.apache.iotdb + mybatis-support + ${project.version} + com.baomidou - mybatis-plus-spring-boot3-starter + ${mybatisplus-starter} ${mybatisplus.version} + + org.apache.velocity + velocity-engine-core + 2.4.1 + com.baomidou mybatis-plus-generator @@ -55,7 +91,6 @@ org.apache.iotdb iotdb-jdbc - ${iotdb-jdbc.version} commons-logging @@ -66,37 +101,20 @@ org.springframework.boot spring-boot-starter - ${spring-boot.version} org.springframework.boot - spring-boot-starter-web + ${web-starter} org.springframework.boot spring-boot-starter-test - ${spring-boot.version} test - - io.springfox - springfox-swagger2 - ${io-springfox.version} - - - io.springfox - springfox-swagger-ui - ${io-springfox.version} - org.projectlombok lombok - 1.18.36 - - - com.github.jeffreyning - mybatisplus-plus - 1.7.5-RELEASE + true ch.qos.logback @@ -110,27 +128,63 @@ - org.mybatis.generator - mybatis-generator-maven-plugin - 1.4.2 - - - org.apache.iotdb - mybatis-generator-plugin - 2.0.4-SNAPSHOT - - + org.apache.maven.plugins + maven-compiler-plugin - true - true - src/main/resources/generatorConfig.xml + true - com.diffplug.spotless - spotless-maven-plugin - 2.43.0 + org.apache.maven.plugins + maven-surefire-plugin + + 3.5.3 + + + + integration-tests + none + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.5.3 + + + spring-boot4 + + 4.1.1 + 7.0.9 + 2.21.5 + 2.21 + 5.23.0 + mybatis-plus-spring-boot4-starter + spring-boot-starter-webmvc + + + + iotdb-mybatis-it + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + + + + + 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 00000000..7c58558b --- /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 tables, Path output) + throws SQLException { + Map> schemas = new LinkedHashMap<>(); + try (Connection connection = source.getConnection()) { + for (String table : tables) { + List 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 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 templateData(List columns) { + java.util.Set 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 keys = columns.stream().filter(Column::key).collect(Collectors.toList()); + List tags = + columns.stream().filter(c -> c.category.equals("TAG")).collect(Collectors.toList()); + List fields = + columns.stream().filter(c -> c.category.equals("FIELD")).collect(Collectors.toList()); + List 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 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 -> + "" + + xml(quote(c.name)) + + ",") + .collect(Collectors.joining("\n"))); + data.put( + "iotdbUpsertValues", + keys.stream().map(c -> c.parameter("row") + ",").collect(Collectors.joining("\n")) + + "\n" + + fields.stream() + .map( + c -> + "" + + c.parameter("row") + + ",") + .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 "AND " + + xml(quote(c.name)) + + " = " + + c.parameter(parameter) + + "AND " + + xml(quote(c.name)) + + " IS NULL"; + } + + 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("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """); + } + + 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 9db7b828..acd8f35d 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 00000000..82d1295d --- /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 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 9e324d08..47ad9b35 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 2713354d..6ec59e3d 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 00000000..f20f195f --- /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 extends Mapper { + int insert(T entity); + + List selectList(@Param(Constants.WRAPPER) Wrapper 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 40c0f26a..3dcbf5a7 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 {} +@Mapper +public interface Table1Mapper extends IoTDBTableMapper {} 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 bf9ba7d2..cd2909ea 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 {} +@Mapper +public interface Table2Mapper extends IoTDBTableMapper {} 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 bcd980bb..9cb95599 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 {} +public interface Table1Service { + int insert(Table1 row); + + Table1 selectByKey(Table1 key); + + int deleteByKey(Table1 key); + + int upsertFields(Table1 row); + + int updateAttributes(Table1 row); + + List 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 bc630696..908e260f 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 {} +public interface Table2Service { + int insert(Table2 row); + + Table2 selectByKey(Table2 key); + + int deleteByKey(Table2 key); + + int upsertFields(Table2 row); + + int updateAttributes(Table2 row); + + List 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 8ccde7e8..ddc84f3b 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.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 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 list() { + return mapper.selectList( + new QueryWrapper() + .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 6de7a682..a4f06c8a 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.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 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 list() { + return mapper.selectList( + new QueryWrapper() + .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 77532ccc..00000000 --- a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/xml/Table1Mapper.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - 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 ea6a9593..00000000 --- a/examples/mybatisplus-generator/src/main/java/org/apache/iotdb/xml/Table2Mapper.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - diff --git a/examples/mybatisplus-generator/src/main/resources/application.yml b/examples/mybatisplus-generator/src/main/resources/application.yml index bdf355a3..5a7c0f32 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 00000000..db275b71 --- /dev/null +++ b/examples/mybatisplus-generator/src/main/resources/mappers/Table1Mapper.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + "time" = #{key.time,jdbcType=TIMESTAMP} + + AND "region" = #{key.region,jdbcType=VARCHAR} + AND "region" IS NULL + + + AND "plant_id" = #{key.plantId,jdbcType=VARCHAR} + AND "plant_id" IS NULL + + + AND "device_id" = #{key.deviceId,jdbcType=VARCHAR} + AND "device_id" IS NULL + + + + DELETE FROM "table1" WHERE + + + INSERT INTO "table1" + "time", "region", "plant_id", "device_id", + "temperature", + "humidity", + "status", + "arrival_time", + "reading_date", + "payload", + VALUES + #{row.time,jdbcType=TIMESTAMP}, #{row.region,jdbcType=VARCHAR}, #{row.plantId,jdbcType=VARCHAR}, #{row.deviceId,jdbcType=VARCHAR}, + #{row.temperature,jdbcType=FLOAT}, + #{row.humidity,jdbcType=FLOAT}, + #{row.status,jdbcType=BOOLEAN}, + #{row.arrivalTime,jdbcType=TIMESTAMP}, + #{row.readingDate,jdbcType=DATE,typeHandler=org.apache.iotdb.mybatis.type.IoTDBLocalDateTypeHandler}, + #{row.payload,jdbcType=BLOB,typeHandler=org.apache.iotdb.mybatis.type.IoTDBBlobTypeHandler}, + + + UPDATE "table1" SET "model_id" = #{row.modelId,jdbcType=VARCHAR}, "maintenance" = #{row.maintenance,jdbcType=VARCHAR} + + + AND "region" = #{row.region,jdbcType=VARCHAR} + AND "region" IS NULL + + + AND "plant_id" = #{row.plantId,jdbcType=VARCHAR} + AND "plant_id" IS NULL + + + AND "device_id" = #{row.deviceId,jdbcType=VARCHAR} + AND "device_id" IS NULL + + + + 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 00000000..4c911389 --- /dev/null +++ b/examples/mybatisplus-generator/src/main/resources/mappers/Table2Mapper.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + "time" = #{key.time,jdbcType=TIMESTAMP} + + AND "region" = #{key.region,jdbcType=VARCHAR} + AND "region" IS NULL + + + AND "plant_id" = #{key.plantId,jdbcType=VARCHAR} + AND "plant_id" IS NULL + + + AND "device_id" = #{key.deviceId,jdbcType=VARCHAR} + AND "device_id" IS NULL + + + + DELETE FROM "table2" WHERE + + + INSERT INTO "table2" + "time", "region", "plant_id", "device_id", + "temperature", + "humidity", + "status", + "arrival_time", + "reading_date", + "payload", + VALUES + #{row.time,jdbcType=TIMESTAMP}, #{row.region,jdbcType=VARCHAR}, #{row.plantId,jdbcType=VARCHAR}, #{row.deviceId,jdbcType=VARCHAR}, + #{row.temperature,jdbcType=FLOAT}, + #{row.humidity,jdbcType=FLOAT}, + #{row.status,jdbcType=BOOLEAN}, + #{row.arrivalTime,jdbcType=TIMESTAMP}, + #{row.readingDate,jdbcType=DATE,typeHandler=org.apache.iotdb.mybatis.type.IoTDBLocalDateTypeHandler}, + #{row.payload,jdbcType=BLOB,typeHandler=org.apache.iotdb.mybatis.type.IoTDBBlobTypeHandler}, + + + UPDATE "table2" SET "model_id" = #{row.modelId,jdbcType=VARCHAR}, "maintenance" = #{row.maintenance,jdbcType=VARCHAR} + + + AND "region" = #{row.region,jdbcType=VARCHAR} + AND "region" IS NULL + + + AND "plant_id" = #{row.plantId,jdbcType=VARCHAR} + AND "plant_id" IS NULL + + + AND "device_id" = #{row.deviceId,jdbcType=VARCHAR} + AND "device_id" IS NULL + + + + 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 00000000..d5b75fbe --- /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 00000000..d5dcf243 --- /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 00000000..328c97fe --- /dev/null +++ b/examples/mybatisplus-generator/src/main/resources/templates/iotdb-mapper.xml.vm @@ -0,0 +1,41 @@ + + + + + +${iotdbResults} + + + + DELETE FROM ${iotdbTable} ${iotdbKeyPredicate} + + + INSERT INTO ${iotdbTable} + ${iotdbUpsertColumns} + VALUES + ${iotdbUpsertValues} + +#if($iotdbHasAttributes) + + UPDATE ${iotdbTable} SET ${iotdbAttributes} + ${iotdbTagPredicate} + +#end + 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 9b9b121f..fb9da66a 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 00000000..941c30ba --- /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 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 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 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", ""readings""); + 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"); + 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 data = + CodeGenerator.templateData( + List.of( + new CodeGenerator.Column("time", "TIMESTAMP", "TIME"), + new CodeGenerator.Column("order", "STRING", "TAG"))); + assertThat(data.get("iotdbColumns")).isEqualTo(""time", "order""); + 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 00000000..84a0a1d4 --- /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 args = + new ArrayList<>( + List.of("-classpath", System.getProperty("java.class.path"), "-d", classes.toString())); + try (java.util.stream.Stream 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 13a72c5e..a3ce42c0 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -81,8 +81,8 @@ org.apache.maven.plugins maven-compiler-plugin - 8 - 8 + 17 + 17 diff --git a/examples/rabbitmq/readme.md b/examples/rabbitmq/readme.md index a53ddf68..32293567 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 org.apache.iotdb iotdb-session - 2.0.5 + 2.0.11 com.rabbitmq @@ -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 b9a8bb8e..84e9bfea 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 org.apache.iotdb iotdb-session - 2.0.5 + 2.0.11 org.apache.rocketmq diff --git a/iotdb-collector/collector-core/pom.xml b/iotdb-collector/collector-core/pom.xml index 205fbec0..64c78105 100644 --- a/iotdb-collector/collector-core/pom.xml +++ b/iotdb-collector/collector-core/pom.xml @@ -30,6 +30,10 @@ IoTDB: Collector: Core 2.0.4-SNAPSHOT + + org.apache.iotdb + iotdb-subscription + org.apache.iotdb collector-openapi @@ -113,10 +117,6 @@ org.apache.iotdb iotdb-thrift - - org.apache.iotdb - iotdb-session - org.apache.thrift libthrift @@ -193,8 +193,8 @@ org.apache.maven.plugins maven-compiler-plugin - 8 - 8 + 17 + 17 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 bee813a7..d95faced 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 @@ 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 Iterable processTablet( throw new UnsupportedOperationException(); } + @Override + public Iterable processTabletWithCollect( + final BiConsumer 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 7b97703a..77655b1b 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,20 @@ 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.SubscriptionTreeSession; 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 +53,7 @@ public class IoTDBPushSource extends PushSource { private String deviceId; private volatile boolean isStarted = true; + private SubscriptionTreePullConsumer consumer; private Thread workerThread; @Override @@ -76,42 +82,96 @@ public void customize( @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 SubscriptionTreeSession session = new SubscriptionTreeSession(host, port)) { + session.open(); + final Optional 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 messages = consumer.poll(timeout); + final List 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 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/source/pull/PullSourceTask.java b/iotdb-collector/collector-core/src/main/java/org/apache/iotdb/collector/runtime/task/source/pull/PullSourceTask.java index 5cf7468e..423595cd 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 @@ -99,8 +99,10 @@ public void createInternal() throws Exception { consumers[i].consumer().close(); } catch (final Exception ex) { LOGGER.warn("Failed to close source on creation failure", ex); - throw e; } + // Like SinkTask/ProcessorTask: a source that cannot start must fail task creation + // instead of being swallowed when its cleanup succeeds. + throw e; } int finalI = i; 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 1c74234b..ce0647b1 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 @@ -81,8 +81,10 @@ public void createInternal() throws Exception { pushSources[i].close(); } catch (final Exception ex) { LOGGER.warn("Failed to close source on creation failure", ex); - throw e; } + // Like SinkTask/ProcessorTask: a source that cannot start must fail task creation + // instead of being swallowed when its cleanup succeeds. + throw e; } } diff --git a/iotdb-spring-boot-starter/README.md b/iotdb-spring-boot-starter/README.md index d3ab8af9..8894c980 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 + + org.apache.iotdb + iotdb-spring-boot-starter + 2.0.4-SNAPSHOT + ``` - - - org.springframework.boot - spring-boot-starter - - - org.springframework.boot - spring-boot-starter-test - test - - - org.apache.iotdb - iotdb-spring-boot-starter - 2.0.3 - - + +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 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 83a75dec..85552d48 100644 --- a/iotdb-spring-boot-starter/pom.xml +++ b/iotdb-spring-boot-starter/pom.xml @@ -35,8 +35,9 @@ 17 UTF-8 3.5.1 - 6.2.7 - 2.0.5 + 4.1.1 + + 6.2.8 @@ -79,11 +80,32 @@ org.springframework.boot spring-boot-configuration-processor ${spring-boot.version} + true org.springframework spring-context - ${spring.version} + + + junit + junit + test + + + org.springframework.boot + spring-boot-test + ${spring-boot.version} + test + + + org.assertj + assertj-core + test + + + org.springframework + spring-beans + test @@ -105,4 +127,13 @@ + + + spring-boot4 + + ${spring-boot4.version} + 7.0.9 + + + 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 5c2a3e06..efa26d31 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 @@ 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 void setDatabase(String database) { 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 4a56429f..2d741e90 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.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 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 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 00000000..73985711 --- /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 00000000..8e7804c3 --- /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 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 tree = new AtomicReference<>(); + AtomicReference 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 pool(Class 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 f8904667..050f1ae0 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 9e0b8114..827f5607 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:-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 016d7c17..ba5bb749 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 f0014849..0359a633 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 898c0a82..50c70e20 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 a16b755e..3fbba631 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 reactor | `org.apache.iotdb:iotdb-extras-parent` | `2.0.4-SNAPSHOT` | module `pom.xml` `` | -| IoTDB session client | `org.apache.iotdb:iotdb-session` | `2.0.5` | inherited from parent `iotdb.version`; module declares the dependency with no `` | -| 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 `` | +| 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 b96e57e4..df84f8cf 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 63e8f7bf..6e1f39d9 100644 --- a/iotdb-thingsboard-table/pom.xml +++ b/iotdb-thingsboard-table/pom.xml @@ -36,15 +36,9 @@ 17 UTF-8 + com.google.guava 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 407b5398..a8ab46c5 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 @@ private static ITableSessionPool buildSessionPool(IoTDBTableConfig config) { .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 f7c9a27e..598639c9 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 @@ private String buildBucketAggregationSql( 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 @@ private static String aggregationProjection(Aggregation aggregation) { + ", " + 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 @@ private static String aggregationProjection(Aggregation aggregation) { + 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 3b160fa7..a0baddcb 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 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 @@ @Tag("integration") @Testcontainers(disabledWithoutDocker = true) class IoTDBTableAttributesDaoIT { + private final List 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 @@ private TestScope scope(String databasePrefix, String entityId) { 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 708d6b4b..e0187b00 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.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 @@ *

TC-1 is defined with two profiles. This class is the smoke profile: a local, fast, * JUnit-driven run that exercises the real {@link IoTDBTableTimeseriesDao#save} path (bounded queue * → single flush worker → multi-row {@code Tablet} insert → 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. * *

The >10K writes/sec headline target is the FULL profile number on a dedicated host. @@ -79,6 +80,7 @@ @Tag("integration") @Testcontainers(disabledWithoutDocker = true) class IoTDBTableIngestionBenchmarkIT { + private final List 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 @@ private BenchmarkScope scope() { 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 a4c329ad..880ea2a5 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.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 @@ @Tag("integration") @Testcontainers(disabledWithoutDocker = true) class IoTDBTableLatestDaoIT { + private final List 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 @@ private TestScope scope(String databasePrefix, String tenantId, String entityId) 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 aad140be..4bf100d4 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.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.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(ms, time, )} + {@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 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 @@ private TestScope scope(String databasePrefix, String tenantId, String entityId) 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 46e0aad8..cc9bf778 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 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.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 @@ @Tag("integration") @Testcontainers(disabledWithoutDocker = true) class IoTDBTableTimeseriesDaoIT { + private final List 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 @@ static void restoreRawOnlyBackendProperties() { } /** - * 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 @@ private TestScope scope(String databasePrefix, String tenantId, String entityId) 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 66fa857f..b3dc70a5 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 @@ private boolean isNull(String column) { 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 bd6cf4f0..fde0ac74 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.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 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.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: * *

    @@ -61,6 +63,7 @@ @Tag("integration") @Testcontainers(disabledWithoutDocker = true) class IoTDBTableTtlIT { + private final List 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 @@ private void awaitIoTDBReady(String database) throws Exception { "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 c91e773a..bee354a6 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--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 - - - - org.mybatis.generator - mybatis-generator-maven-plugin - 1.4.2 - - - org.apache.iotdb - mybatis-generator-plugin - 2.0.3 - - - - true - true - src/main/resources/generatorConfig.xml - - - - + + + + + ``` -- `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)` 是内部单批映射方法,直接调用会绕过校验和拆批。**从旧版升级时,必须同时重新生成 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 4f60b9c2..713164bd 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--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 + + org.mybatis.generator + mybatis-generator-maven-plugin + 1.4.2 + + + org.apache.iotdb + mybatis-generator-plugin + 2.0.4-SNAPSHOT + + + org.apache.iotdb + iotdb-jdbc + 2.0.11 + + + + src/main/resources/generatorConfig.xml + true + true + + +``` + +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 - - - - org.mybatis.generator - mybatis-generator-maven-plugin - 1.4.2 - - - org.apache.iotdb - mybatis-generator-plugin - 2.0.3 - - - - true - true - src/main/resources/generatorConfig.xml - - - - + + + + + ``` -- 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)` 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 26a108e7..7df748dd 100644 --- a/mybatis-generator/pom.xml +++ b/mybatis-generator/pom.xml @@ -19,10 +19,21 @@ + + org.mybatis + mybatis + 3.5.19 + test + org.mybatis.generator mybatis-generator-core 1.4.2 + + junit + junit + test + 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 cb24f3cd..f3cf80ca 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.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 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 list) { - return true; + public boolean validate(List 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 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 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 @@ private void batchInsertMethod(Interface interfaze, IntrospectedTable introspect 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 columns = introspectedTable.getAllColumns(); - String incrementField = - introspectedTable.getTableConfiguration().getProperties().getProperty("incrementField"); - if (incrementField != null) { - incrementField = incrementField.toUpperCase(); - } + List 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 @@ private void addBatchInsertXml(Document document, IntrospectedTable introspected 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 00000000..2a1a0d64 --- /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 warnings = new ArrayList<>(); + + @Override + public boolean validate(List 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 650d4ec6..9e434430 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 boolean modelBaseRecordClassGenerated( 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 78b9d73c..5dd578a8 100644 --- a/mybatis-generator/src/main/resources/generatorConfig.xml +++ b/mybatis-generator/src/main/resources/generatorConfig.xml @@ -18,7 +18,7 @@ --> - + @@ -29,6 +29,8 @@ value="org.mybatis.generator.api.dom.DefaultXmlFormatter"/>--> + + @@ -53,7 +55,7 @@ - +
    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 00000000..42e27e43 --- /dev/null +++ b/mybatis-generator/src/test/java/org/apache/iotdb/mybatis/plugin/BatchInsertPluginTest.java @@ -0,0 +1,270 @@ +/* + * 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.StringReader; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Proxy; +import java.net.URLClassLoader; +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 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 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 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 StringReader(xml), 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(" 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 StringReader(new DefaultXmlFormatter().getFormattedContent(document)), + 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 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 00000000..ef7c5763 --- /dev/null +++ b/mybatis-support/README.md @@ -0,0 +1,102 @@ + + +# 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--mybatis-generator-plugin-bin.zip`. + +Add this application dependency (it is separate from the build-time generator plugin): + +```xml + + org.apache.iotdb + mybatis-support + 2.0.4-SNAPSHOT + +``` + +Register the query interceptor on the IoTDB MyBatis configuration: + +```xml + + + +``` + +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 + + +``` + +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 00000000..41063f99 --- /dev/null +++ b/mybatis-support/pom.xml @@ -0,0 +1,40 @@ + + + + 4.0.0 + + org.apache.iotdb + iotdb-extras-parent + 2.0.4-SNAPSHOT + + mybatis-support + IoTDB Extras: MyBatis Runtime Support + + + org.mybatis + mybatis + 3.5.19 + provided + + + junit + junit + test + + + 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 00000000..f9724c36 --- /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 00000000..e7878229 --- /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 { + @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 00000000..a516c661 --- /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 { + @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 00000000..7f128014 --- /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 00000000..c6df49f4 --- /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 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 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 proxy(Class 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 b155418b..6704b40b 100644 --- a/pom.xml +++ b/pom.xml @@ -38,32 +38,30 @@ iotdb-collector metric-scrape mybatis-generator + mybatis-support - 1.7 206 0.9 - - 4.9.3 + 4.13.2 4.2.0 - 2.9.3 3.3.0 3.38.0 Release 1.5.0 - 1.16.0 + 1.16.1 1.10.0 2.14.0 3.3 - 3.13.0 + 3.18.0 3.6.1 2.11.1 4.4 @@ -79,8 +77,8 @@ 3.8.1-17.0 1.17.1 1.16 - 1.22.0 - 2.10.1 + 1.28.0 + 2.13.1 32.1.2-jre 3.3.6 3.1.3 @@ -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 --> - 0.14.1.0 - 2.0.5 - 2.16.2 + 0.23.0.0 + 2.0.11 + 2.18.6 4.0.4 @@ -103,48 +101,39 @@ 1.0-1 2.40 - 9.4.57.v20241219 0.11.5 - 3.23.0 + 3.26.2 5.14.0 2.5.0 3.1 4.13.2 2.8.2 - - 1.3.15 - 1.8.0 + 1.5.34 + 1.10.1 3.6.0 - 1.8 - 1.8 + 17 + 17 + 17 1.11.4 - 0.6.11 + 0.6.14 2.23.4 - - 0.17 - 4.1.97.Final + 0.18.0 + 4.1.134.Final 9.37.2 10.15 - - 6.6.0 + 7.10.0 1.9.1 7.0.0 1.5.6 2.0.9 - 1.0.5 - - 3.1.0-a6a21d5-SNAPSHOT + 1.1.0 + 3.3.0 1.0.4 - 1.1.13 - 3.5.10 + 1.2.9 + 3.7.9 0.10.2 5.1.3 2.12.19 @@ -152,7 +141,7 @@ 3.0.9 2.0.9 - 1.1.10.4 + 1.1.10.5 target/jacoco-merged-reports/jacoco.xml **/generated-sources @@ -165,29 +154,22 @@ 3.5.0 false - 2.43.0 - + 2.44.5 + 2.7.18 5.3.39 3.49.1.0 - 1.6.14 chmod - - 0.14.1 - + + 0.23.0 9.0.86 - 2.1.1 + 2.4.0 1.9 0.11.1 - 1.5.5-5 + 1.5.6-3 + + + io.netty + netty-bom + ${netty.version} + pom + import + + + com.fasterxml.jackson + jackson-bom + ${jackson.version} + pom + import + + + at.yawk.lz4 + lz4-java + ${lz4-java.version} + + + org.xerial.snappy + snappy-java + ${snappy-java.version} + + + org.apache.iotdb + iotdb-jdbc + ${iotdb.version} + + + org.apache.iotdb + iotdb-subscription + ${iotdb.version} + + + org.apache.iotdb + isession + ${iotdb.version} + org.springframework.boot spring-boot @@ -251,15 +273,38 @@ libthrift ${thrift.version} - + + + org.apache.httpcomponents.client5 + httpclient5 + - org.apache.tomcat.embed - tomcat-embed-core + org.apache.httpcomponents.core5 + httpcore5 - - javax.annotation - javax.annotation-api + org.apache.httpcomponents.core5 + httpcore5-h2 + + + jakarta.servlet + jakarta.servlet-api + + + + jakarta.annotation + jakarta.annotation-api + + + + org.apache.commons + commons-lang3 @@ -1100,7 +1145,7 @@ com.fasterxml.jackson.module jackson-module-jaxb-annotations - 2.15.2 + ${jackson.version} @@ -1693,6 +1738,23 @@ + + + enforce-java-baseline + + enforce + + validate + + false + + + [17,) + IoTDB Extras with IoTDB 2.0.11 requires JDK 17 or newer. + + + + enforce-version-convergence @@ -1741,7 +1803,7 @@ true - 1.8.0 + [17,) @@ -2025,43 +2087,8 @@ - - .java-9-and-above - - [9,) - - - 8 - - - - - .java-11-below - - (,11] - - - - 2.27.1 - - true - - - - - .java-16 - - 16 - - - --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.javac.file=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED - - @@ -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). --> with-thingsboard