This is an automated email from the ASF dual-hosted git repository. CritasWang pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/iotdb-client-rust.git
commit 2e25d013a46f0262c6a4c60c6b2b858dc9993e42 Author: CritasWang <[email protected]> AuthorDate: Fri Jul 10 17:50:38 2026 +0800 Phase 6: README EN/ZH, runnable examples, GitHub Actions CI, Apache 2.0 license headers; fix null-bitmap size at 8-row boundary --- .github/workflows/ci.yml | 86 +++++++++++++++++++ Cargo.toml | 17 ++++ LICENSE | 201 +++++++++++++++++++++++++++++++++++++++++++ README.md | 212 +++++++++++++++++++++++++++++++++++++++++---- README_ZH.md | 213 ++++++++++++++++++++++++++++++++++++++++++++++ examples/session_pool.rs | 107 +++++++++++++++++++++++ examples/table_session.rs | 115 +++++++++++++++++++++++++ examples/tree_session.rs | 111 ++++++++++++++++++++++++ src/data/tablet.rs | 21 +++-- src/error.rs | 17 ++++ src/lib.rs | 17 ++++ src/protocol/client.rs | 17 ++++ src/protocol/common.rs | 17 ++++ src/protocol/mod.rs | 17 ++++ tools/check-license.sh | 51 +++++++++++ tools/generate-thrift.sh | 11 +++ 16 files changed, 1206 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fbcfa5a --- /dev/null +++ b/.github/workflows/ci.yml @@ -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. + +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + CARGO_TERM_COLOR: always + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - uses: Swatinem/rust-cache@v2 + + - name: License header check + run: ./tools/check-license.sh + + - name: Format check + run: cargo fmt --check + + - name: Clippy + run: cargo clippy --all-targets -- -D warnings + + - name: Build + run: cargo build + + # Live-server tests skip themselves when nothing listens on 6667. + - name: Unit tests + run: cargo test + + integration: + runs-on: ubuntu-latest + services: + iotdb: + image: apache/iotdb:2.0.6-standalone + ports: + - 6667:6667 + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + + - name: Wait for IoTDB + run: | + for i in $(seq 1 60); do + if (exec 3<>/dev/tcp/127.0.0.1/6667) 2>/dev/null; then + exec 3>&- 3<&- + echo "IoTDB is up after ${i}s" + exit 0 + fi + sleep 1 + done + echo "IoTDB did not become ready in 60s" >&2 + exit 1 + + # Includes the live-server tests now that IoTDB is reachable. + - name: Integration tests + run: cargo test diff --git a/Cargo.toml b/Cargo.toml index 2a071d1..23195dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,20 @@ +# 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] name = "iotdb-client" version = "0.1.0" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 52a827e..486601d 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,213 @@ # Apache IoTDB Rust Client -Rust client SDK for [Apache IoTDB](https://iotdb.apache.org/), speaking Apache Thrift RPC (default port 6667). Supports the **tree model** (`Session`, device/timeseries paths) with the **table model** (`TableSession`, SQL dialect) planned — mirroring the architecture of the Node.js and C# SDKs. +[](https://www.apache.org/licenses/LICENSE-2.0) + +[English](./README.md) | [中文](./README_ZH.md) + +Rust client SDK for [Apache IoTDB](https://iotdb.apache.org/), speaking Apache Thrift RPC (default port 6667). Supports both IoTDB data models, mirroring the architecture of the Node.js and C# SDKs: + +- **Tree model** — `Session` / `SessionPool`: device/timeseries paths (`root.sg.d1.s1`) +- **Table model** — `TableSession` / `TableSessionPool`: relational SQL dialect ## Status -Early scaffold. Thrift stubs are not yet generated; session layer is a skeleton. +Working client: session management (with multi-node failover), tablet writes (`insertTablet`) for both models, TsBlock query decoding with paging iteration, and thread-safe session pools. Not yet published to crates.io. + +## Requirements + +- Rust 1.75+ +- Apache IoTDB 2.x (examples and integration tests use `apache/iotdb:2.0.6-standalone`) + +## Installation + +Once published to crates.io: + +```toml +[dependencies] +iotdb-client = "0.1" +``` + +Until then, use a git dependency: + +```toml +[dependencies] +iotdb-client = { git = "https://github.com/apache/iotdb-client-rust" } +``` + +## Quick start + +### Tree model + +```rust +use iotdb_client::{Result, Session, SessionConfig, TSDataType, Tablet, Value}; + +fn main() -> Result<()> { + let config = SessionConfig::default().with_node_urls(&["127.0.0.1:6667"])?; + let mut session = Session::new(config); + session.open()?; + + session.execute_non_query("CREATE DATABASE root.demo")?; + session.execute_non_query( + "CREATE TIMESERIES root.demo.d1.temperature WITH DATATYPE=DOUBLE, ENCODING=PLAIN", + )?; + + // Batch write via a column-major tablet (nulls allowed). + let mut tablet = Tablet::new( + "root.demo.d1", + vec!["temperature".into()], + vec![TSDataType::Double], + )?; + tablet.add_row(1_720_000_000_000, vec![Some(Value::Double(21.5))])?; + tablet.add_row(1_720_000_001_000, vec![None])?; // null cell + session.insert_tablet(&tablet)?; + + // Query with row iteration; the dataset borrows the session until dropped. + { + let mut dataset = session.execute_query("SELECT temperature FROM root.demo.d1")?; + while let Some(row) = dataset.next_row()? { + println!("ts={:?} values={:?}", row.timestamp, row.values); + } + } + + session.execute_non_query("DELETE DATABASE root.demo")?; + session.close() +} +``` + +### Table model + +```rust +use iotdb_client::{ColumnCategory, Result, TSDataType, TableSession, Tablet, Value}; + +fn main() -> Result<()> { + let mut session = TableSession::builder() + .node_urls(&["127.0.0.1:6667"])? + .username("root") + .password("root") + .build()?; -## Build & Test + session.execute_non_query("CREATE DATABASE IF NOT EXISTS demo")?; + session.execute_non_query("USE demo")?; + session.execute_non_query( + "CREATE TABLE IF NOT EXISTS sensors (device_id STRING TAG, temperature DOUBLE FIELD)", + )?; + + let mut tablet = Tablet::new_table( + "sensors", + vec!["device_id".into(), "temperature".into()], + vec![TSDataType::String, TSDataType::Double], + vec![ColumnCategory::Tag, ColumnCategory::Field], + )?; + tablet.add_row( + 1_720_000_000_000, + vec![ + Some(Value::String("dev-1".into())), + Some(Value::Double(21.5)), + ], + )?; + session.insert(&tablet)?; + + { + let mut dataset = session.execute_query("SELECT time, device_id, temperature FROM sensors")?; + while let Some(row) = dataset.next_row()? { + println!("{:?}", row.values); + } + } + + session.execute_non_query("DROP DATABASE demo")?; + session.close() +} +``` + +### Session pool + +```rust +use std::sync::Arc; +use iotdb_client::{Result, SessionPool, SessionPoolConfig}; + +fn main() -> Result<()> { + let config = SessionPoolConfig { + max_size: 4, + ..SessionPoolConfig::default() + } + .with_node_urls(&["127.0.0.1:6667"])?; + let pool = Arc::new(SessionPool::new(config)?); + + let handles: Vec<_> = (0..4) + .map(|_| { + let pool = Arc::clone(&pool); + std::thread::spawn(move || -> Result<()> { + let mut session = pool.acquire()?; // RAII guard, released on drop + session.execute_non_query("SHOW DATABASES")?; + Ok(()) + }) + }) + .collect(); + for handle in handles { + handle.join().expect("thread panicked")?; + } + + pool.close(); + Ok(()) +} +``` + +Full runnable versions live in [`examples/`](./examples): ```sh -cargo build -cargo test # unit tests -cargo test test_name # single test -cargo fmt && cargo clippy # format + lint +cargo run --example tree_session +cargo run --example table_session +cargo run --example session_pool ``` ## Thrift codegen -IDL sources in `thrift/` (`client.thrift`, `common.thrift`) originate from the IoTDB repo's `iotdb-protocol/`. Regenerate stubs (requires the `thrift` compiler ≥ 0.23): +Generated stubs live in `src/protocol/` (`client.rs`, `common.rs`); never hand-edit them. The IDL sources in `thrift/` are synced from the IoTDB repo's `iotdb-protocol/` (`thrift-datanode/src/main/thrift/client.thrift`, `thrift-commons/src/main/thrift/common.thrift`). + +Regenerate with: ```sh -thrift --gen rs -out src/protocol thrift/common.thrift -thrift --gen rs -out src/protocol thrift/client.thrift +./tools/generate-thrift.sh ``` -Never hand-edit generated files. +The script picks the Thrift compiler in order of preference: + +1. `$THRIFT_BIN` if set +2. the IoTDB repo's Maven build output (`$IOTDB_REPO`, default `../iotdb`): `iotdb-protocol/*/target/thrift/bin/thrift` — run `./mvnw generate-sources -pl iotdb-protocol/thrift-datanode -am` there first. This guarantees the exact Thrift version pinned by the IoTDB pom. +3. `thrift` on `PATH` (version must match the IoTDB pom's `thrift.version`) + +When `$IOTDB_REPO` is present, the IDL files are re-synced from it before generation, and the Apache license headers are re-prepended to the generated files. + +## Development + +```sh +cargo build # build +cargo test # unit tests (live tests self-skip without a server) +cargo test test_name # single test +cargo fmt --check # format check +cargo clippy --all-targets -- -D warnings # lint +./tools/check-license.sh # license header check +``` + +Integration tests need a running IoTDB; the live tests detect it on `127.0.0.1:6667` and skip gracefully when absent: + +```sh +docker compose up -d # standalone IoTDB (see docker-compose-1c1d.yml for a 1C1D cluster) +cargo test # now includes the live-server tests +``` + +## Project layout + +| Path | Contents | +| --- | --- | +| `src/client/` | `Session`, `TableSession`, `SessionPool`, `TableSessionPool`, `SessionDataSet` | +| `src/connection/` | Low-level Thrift transport (framed transport + binary protocol) | +| `src/data/` | `Tablet`, `Value`, `TSDataType` (official TSFile codes 0–11), TsBlock decoding, bitmaps | +| `src/protocol/` | Generated Thrift stubs (do not edit) | +| `thrift/` | Thrift IDL sources, synced from the IoTDB repo | +| `examples/` | Runnable examples for both models and the pools | +| `tools/` | Codegen and license-check scripts | -## Layout +## License -- `src/client/` — Session / pool layer (tree & table model) -- `src/connection/` — low-level Thrift transport (framed + binary protocol) -- `src/data/` — Tablet, SessionDataSet, TSDataType (official TSFile codes 0–11) -- `src/protocol/` — generated Thrift stubs -- `thrift/` — Thrift IDL sources +[Apache License 2.0](./LICENSE) diff --git a/README_ZH.md b/README_ZH.md new file mode 100644 index 0000000..7f34309 --- /dev/null +++ b/README_ZH.md @@ -0,0 +1,213 @@ +# Apache IoTDB Rust 客户端 + +[](https://www.apache.org/licenses/LICENSE-2.0) + +[English](./README.md) | [中文](./README_ZH.md) + +[Apache IoTDB](https://iotdb.apache.org/) 的 Rust 客户端 SDK,基于 Apache Thrift RPC(默认端口 6667)。支持 IoTDB 的两种数据模型,架构与 Node.js、C# SDK 保持一致: + +- **树模型** — `Session` / `SessionPool`:设备/时间序列路径(`root.sg.d1.s1`) +- **表模型** — `TableSession` / `TableSessionPool`:关系型 SQL 方言 + +## 状态 + +可用的客户端:会话管理(支持多节点故障转移)、两种模型的 Tablet 批量写入(`insertTablet`)、TsBlock 查询解码及分页迭代、线程安全的会话池。尚未发布到 crates.io。 + +## 环境要求 + +- Rust 1.75+ +- Apache IoTDB 2.x(示例与集成测试使用 `apache/iotdb:2.0.6-standalone`) + +## 安装 + +发布到 crates.io 之后: + +```toml +[dependencies] +iotdb-client = "0.1" +``` + +在此之前,可使用 git 依赖: + +```toml +[dependencies] +iotdb-client = { git = "https://github.com/apache/iotdb-client-rust" } +``` + +## 快速开始 + +### 树模型 + +```rust +use iotdb_client::{Result, Session, SessionConfig, TSDataType, Tablet, Value}; + +fn main() -> Result<()> { + let config = SessionConfig::default().with_node_urls(&["127.0.0.1:6667"])?; + let mut session = Session::new(config); + session.open()?; + + session.execute_non_query("CREATE DATABASE root.demo")?; + session.execute_non_query( + "CREATE TIMESERIES root.demo.d1.temperature WITH DATATYPE=DOUBLE, ENCODING=PLAIN", + )?; + + // 通过列式 Tablet 批量写入(允许 null)。 + let mut tablet = Tablet::new( + "root.demo.d1", + vec!["temperature".into()], + vec![TSDataType::Double], + )?; + tablet.add_row(1_720_000_000_000, vec![Some(Value::Double(21.5))])?; + tablet.add_row(1_720_000_001_000, vec![None])?; // null 单元格 + session.insert_tablet(&tablet)?; + + // 逐行迭代查询结果;数据集在被 drop 之前持有会话的借用。 + { + let mut dataset = session.execute_query("SELECT temperature FROM root.demo.d1")?; + while let Some(row) = dataset.next_row()? { + println!("ts={:?} values={:?}", row.timestamp, row.values); + } + } + + session.execute_non_query("DELETE DATABASE root.demo")?; + session.close() +} +``` + +### 表模型 + +```rust +use iotdb_client::{ColumnCategory, Result, TSDataType, TableSession, Tablet, Value}; + +fn main() -> Result<()> { + let mut session = TableSession::builder() + .node_urls(&["127.0.0.1:6667"])? + .username("root") + .password("root") + .build()?; + + session.execute_non_query("CREATE DATABASE IF NOT EXISTS demo")?; + session.execute_non_query("USE demo")?; + session.execute_non_query( + "CREATE TABLE IF NOT EXISTS sensors (device_id STRING TAG, temperature DOUBLE FIELD)", + )?; + + let mut tablet = Tablet::new_table( + "sensors", + vec!["device_id".into(), "temperature".into()], + vec![TSDataType::String, TSDataType::Double], + vec![ColumnCategory::Tag, ColumnCategory::Field], + )?; + tablet.add_row( + 1_720_000_000_000, + vec![ + Some(Value::String("dev-1".into())), + Some(Value::Double(21.5)), + ], + )?; + session.insert(&tablet)?; + + { + let mut dataset = session.execute_query("SELECT time, device_id, temperature FROM sensors")?; + while let Some(row) = dataset.next_row()? { + println!("{:?}", row.values); + } + } + + session.execute_non_query("DROP DATABASE demo")?; + session.close() +} +``` + +### 会话池 + +```rust +use std::sync::Arc; +use iotdb_client::{Result, SessionPool, SessionPoolConfig}; + +fn main() -> Result<()> { + let config = SessionPoolConfig { + max_size: 4, + ..SessionPoolConfig::default() + } + .with_node_urls(&["127.0.0.1:6667"])?; + let pool = Arc::new(SessionPool::new(config)?); + + let handles: Vec<_> = (0..4) + .map(|_| { + let pool = Arc::clone(&pool); + std::thread::spawn(move || -> Result<()> { + let mut session = pool.acquire()?; // RAII 守卫,drop 时归还 + session.execute_non_query("SHOW DATABASES")?; + Ok(()) + }) + }) + .collect(); + for handle in handles { + handle.join().expect("thread panicked")?; + } + + pool.close(); + Ok(()) +} +``` + +完整可运行版本见 [`examples/`](./examples): + +```sh +cargo run --example tree_session +cargo run --example table_session +cargo run --example session_pool +``` + +## Thrift 代码生成 + +生成的桩代码位于 `src/protocol/`(`client.rs`、`common.rs`),请勿手动编辑。`thrift/` 中的 IDL 源文件从 IoTDB 仓库的 `iotdb-protocol/` 同步(`thrift-datanode/src/main/thrift/client.thrift`、`thrift-commons/src/main/thrift/common.thrift`)。 + +重新生成: + +```sh +./tools/generate-thrift.sh +``` + +脚本按以下优先级选择 Thrift 编译器: + +1. `$THRIFT_BIN`(若已设置) +2. IoTDB 仓库 Maven 构建产物(`$IOTDB_REPO`,默认 `../iotdb`):`iotdb-protocol/*/target/thrift/bin/thrift` — 需先在该仓库执行 `./mvnw generate-sources -pl iotdb-protocol/thrift-datanode -am`。这可保证 Thrift 版本与 IoTDB pom 中固定的版本完全一致。 +3. `PATH` 上的 `thrift`(版本须与 IoTDB pom 的 `thrift.version` 匹配) + +当 `$IOTDB_REPO` 存在时,生成前会先从其重新同步 IDL 文件,并在生成后为生成文件重新添加 Apache 许可证头。 + +## 开发 + +```sh +cargo build # 构建 +cargo test # 单元测试(无服务器时在线测试自动跳过) +cargo test test_name # 运行单个测试 +cargo fmt --check # 格式检查 +cargo clippy --all-targets -- -D warnings # 静态检查 +./tools/check-license.sh # 许可证头检查 +``` + +集成测试需要一个运行中的 IoTDB;在线测试会探测 `127.0.0.1:6667`,服务器不可达时自动跳过: + +```sh +docker compose up -d # 单机版 IoTDB(1C1D 集群拓扑见 docker-compose-1c1d.yml) +cargo test # 此时包含在线测试 +``` + +## 项目结构 + +| 路径 | 内容 | +| --- | --- | +| `src/client/` | `Session`、`TableSession`、`SessionPool`、`TableSessionPool`、`SessionDataSet` | +| `src/connection/` | 底层 Thrift 传输(帧传输 + 二进制协议) | +| `src/data/` | `Tablet`、`Value`、`TSDataType`(官方 TSFile 编码 0–11)、TsBlock 解码、位图 | +| `src/protocol/` | 生成的 Thrift 桩代码(勿编辑) | +| `thrift/` | Thrift IDL 源文件,从 IoTDB 仓库同步 | +| `examples/` | 两种模型及会话池的可运行示例 | +| `tools/` | 代码生成与许可证检查脚本 | + +## 许可证 + +[Apache License 2.0](./LICENSE) diff --git a/examples/session_pool.rs b/examples/session_pool.rs new file mode 100644 index 0000000..968599f --- /dev/null +++ b/examples/session_pool.rs @@ -0,0 +1,107 @@ +// 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. + +//! Session pool walkthrough: share a [`SessionPool`] across threads, each +//! acquiring a session to insert and query concurrently; then the same idea +//! with a [`TableSessionPool`]. +//! +//! Run against a local IoTDB (e.g. `docker compose up -d`): +//! +//! ```sh +//! cargo run --example session_pool +//! ``` + +use std::sync::Arc; +use std::thread; + +use iotdb_client::{ + Result, SessionPool, SessionPoolConfig, TSDataType, TableSessionPool, Tablet, Value, +}; + +const DB: &str = "root.rust_pool_example"; +const THREADS: usize = 4; + +fn main() -> Result<()> { + env_logger::init(); + + // --- Tree-model pool --------------------------------------------------- + let config = SessionPoolConfig { + max_size: THREADS, + min_size: 1, // one session opened eagerly + ..SessionPoolConfig::default() + } + .with_node_urls(&["127.0.0.1:6667"])?; + let pool = Arc::new(SessionPool::new(config)?); + println!("pool created ({} live)", pool.live_count()); + + pool.execute_non_query(&format!("CREATE DATABASE {DB}"))?; + + // Each thread acquires its own session, inserts a tablet for its device + // and queries the row count back. The RAII guard returns the session to + // the pool on drop. + let handles: Vec<_> = (0..THREADS) + .map(|t| { + let pool = Arc::clone(&pool); + thread::spawn(move || -> Result<usize> { + let device = format!("{DB}.d{t}"); + let mut session = pool.acquire()?; + + let mut tablet = + Tablet::new(&device, vec!["value".into()], vec![TSDataType::Int64])?; + let base_ts = 1_720_000_000_000i64; + for i in 0..5i64 { + tablet.add_row(base_ts + i, vec![Some(Value::Int64(t as i64 * 100 + i))])?; + } + session.insert_tablet(&tablet)?; + + let mut rows = 0usize; + let mut dataset = session.execute_query(&format!("SELECT value FROM {device}"))?; + while dataset.next_row()?.is_some() { + rows += 1; + } + Ok(rows) + }) + }) + .collect(); + for (t, handle) in handles.into_iter().enumerate() { + let rows = handle.join().expect("thread panicked")?; + println!("thread {t}: inserted and read back {rows} rows"); + } + + pool.execute_non_query(&format!("DELETE DATABASE {DB}"))?; + pool.close(); + println!("tree pool closed"); + + // --- Table-model pool ---------------------------------------------------- + let config = SessionPoolConfig::default().with_node_urls(&["127.0.0.1:6667"])?; + let table_pool = TableSessionPool::new(config)?; + table_pool.execute_non_query("CREATE DATABASE IF NOT EXISTS rust_pool_example")?; + table_pool.execute_non_query("USE rust_pool_example")?; + { + let mut session = table_pool.acquire()?; + let mut dataset = session.execute_query("SHOW TABLES")?; + println!("tables in rust_pool_example:"); + while let Some(row) = dataset.next_row()? { + println!(" {:?}", row.values); + } + } + table_pool.execute_non_query("DROP DATABASE rust_pool_example")?; + table_pool.close(); + println!("table pool closed"); + + Ok(()) +} diff --git a/examples/table_session.rs b/examples/table_session.rs new file mode 100644 index 0000000..01ef860 --- /dev/null +++ b/examples/table_session.rs @@ -0,0 +1,115 @@ +// 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. + +//! Table-model (relational) walkthrough: open a [`TableSession`] via the +//! builder, create a database and table, insert a table-model [`Tablet`] +//! (TAG + FIELD columns), query it back with SQL, then clean up. +//! +//! Run against a local IoTDB (e.g. `docker compose up -d`): +//! +//! ```sh +//! cargo run --example table_session +//! ``` + +use iotdb_client::{ColumnCategory, Result, TSDataType, TableSession, Tablet, Value}; + +const DB: &str = "rust_example_db"; + +fn main() -> Result<()> { + env_logger::init(); + + // The builder opens the session with sql_dialect="table". + let mut session = TableSession::builder() + .node_urls(&["127.0.0.1:6667"])? + .username("root") + .password("root") + .build()?; + println!("table session opened"); + + // --- DDL: database + table ------------------------------------------ + session.execute_non_query(&format!("CREATE DATABASE IF NOT EXISTS {DB}"))?; + session.execute_non_query(&format!("USE {DB}"))?; + println!("current database: {:?}", session.database()); + session.execute_non_query( + "CREATE TABLE IF NOT EXISTS sensors (\ + region STRING TAG, \ + device_id STRING TAG, \ + temperature DOUBLE FIELD, \ + status BOOLEAN FIELD)", + )?; + println!("table `sensors` created"); + + // --- Insert a table-model tablet (TAG/FIELD categories) -------------- + let mut tablet = Tablet::new_table( + "sensors", + vec![ + "region".into(), + "device_id".into(), + "temperature".into(), + "status".into(), + ], + vec![ + TSDataType::String, + TSDataType::String, + TSDataType::Double, + TSDataType::Boolean, + ], + vec![ + ColumnCategory::Tag, + ColumnCategory::Tag, + ColumnCategory::Field, + ColumnCategory::Field, + ], + )?; + let base_ts = 1_720_000_000_000i64; // epoch milliseconds + for i in 0..8i64 { + tablet.add_row( + base_ts + i * 1_000, + vec![ + Some(Value::String( + if i % 2 == 0 { "east" } else { "west" }.into(), + )), + Some(Value::String(format!("dev-{}", i % 3))), + Some(Value::Double(21.0 + i as f64 * 0.25)), + // Some status readings are missing (null cells). + (i % 3 != 0).then_some(Value::Boolean(i % 2 == 0)), + ], + )?; + } + session.insert(&tablet)?; + println!("inserted {} rows into `sensors`", tablet.row_count()); + + // --- Query with SQL ---------------------------------------------------- + { + let mut dataset = session.execute_query( + "SELECT time, region, device_id, temperature, status \ + FROM sensors ORDER BY time", + )?; + println!("columns: {:?}", dataset.columns()); + while let Some(row) = dataset.next_row()? { + println!("{:?}", row.values); + } + } // dataset drop closes the query and releases the session borrow + + // --- Cleanup ---------------------------------------------------------- + session.execute_non_query(&format!("DROP DATABASE {DB}"))?; + println!("database dropped"); + + session.close()?; + println!("session closed"); + Ok(()) +} diff --git a/examples/tree_session.rs b/examples/tree_session.rs new file mode 100644 index 0000000..32adc07 --- /dev/null +++ b/examples/tree_session.rs @@ -0,0 +1,111 @@ +// 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. + +//! Tree-model walkthrough: open a [`Session`], create a database and +//! timeseries, insert a [`Tablet`] (several data types, some null cells), +//! query it back row by row, then clean up. +//! +//! Run against a local IoTDB (e.g. `docker compose up -d`): +//! +//! ```sh +//! cargo run --example tree_session +//! ``` + +use iotdb_client::{Result, Session, SessionConfig, TSDataType, Tablet, Value}; + +const DB: &str = "root.rust_example"; +const DEVICE: &str = "root.rust_example.d1"; + +fn main() -> Result<()> { + env_logger::init(); + + let config = SessionConfig::default().with_node_urls(&["127.0.0.1:6667"])?; + let mut session = Session::new(config); + session.open()?; + println!("session opened"); + + // --- DDL: database + timeseries ------------------------------------- + session.execute_non_query(&format!("CREATE DATABASE {DB}"))?; + for (name, dtype) in [ + ("temperature", "DOUBLE"), + ("humidity", "FLOAT"), + ("status", "BOOLEAN"), + ("counter", "INT64"), + ("tag", "TEXT"), + ] { + session.execute_non_query(&format!( + "CREATE TIMESERIES {DEVICE}.{name} WITH DATATYPE={dtype}, ENCODING=PLAIN" + ))?; + } + println!("database and timeseries created"); + + // --- Insert a tablet (column-major batch, nulls allowed) ------------ + let mut tablet = Tablet::new( + DEVICE, + vec![ + "temperature".into(), + "humidity".into(), + "status".into(), + "counter".into(), + "tag".into(), + ], + vec![ + TSDataType::Double, + TSDataType::Float, + TSDataType::Boolean, + TSDataType::Int64, + TSDataType::Text, + ], + )?; + let base_ts = 1_720_000_000_000i64; // epoch milliseconds + for i in 0..10i64 { + tablet.add_row( + base_ts + i * 1_000, + vec![ + Some(Value::Double(20.0 + i as f64 * 0.5)), + // Every third humidity reading is missing. + (i % 3 != 0).then_some(Value::Float(40.0 + i as f32)), + Some(Value::Boolean(i % 2 == 0)), + Some(Value::Int64(i * 100)), + // Only some rows carry a text tag. + (i % 4 == 0).then(|| Value::Text(format!("batch-{i}"))), + ], + )?; + } + session.insert_tablet(&tablet)?; + println!("inserted {} rows into {DEVICE}", tablet.row_count()); + + // --- Query and iterate ---------------------------------------------- + { + let mut dataset = session.execute_query(&format!( + "SELECT temperature, humidity, status, counter, tag FROM {DEVICE}" + ))?; + println!("columns: {:?}", dataset.columns()); + println!("types: {:?}", dataset.column_types()); + while let Some(row) = dataset.next_row()? { + println!("ts={:?} values={:?}", row.timestamp, row.values); + } + } // dataset drop closes the query and releases the session borrow + + // --- Cleanup ---------------------------------------------------------- + session.execute_non_query(&format!("DELETE DATABASE {DB}"))?; + println!("database deleted"); + + session.close()?; + println!("session closed"); + Ok(()) +} diff --git a/src/data/tablet.rs b/src/data/tablet.rs index 8cffaf8..4ef1953 100644 --- a/src/data/tablet.rs +++ b/src/data/tablet.rs @@ -194,8 +194,10 @@ impl Tablet { /// Serializes the value buffer per spec §3.2–3.3: all columns /// column-major (nulls occupy placeholder slots), then one trailing /// bitmap entry per column — a flag byte (1 = column has nulls) followed, - /// when flagged, by a `ceil(rows/8)`-byte LSB-first bitmap with bit=1 for - /// null rows. All multi-byte values big-endian. + /// when flagged, by a `rows/8 + 1`-byte LSB-first bitmap with bit=1 for + /// null rows (the server's Java `BitMap` always allocates — and reads — + /// `size/8 + 1` bytes, one extra padding byte when `rows % 8 == 0`). + /// All multi-byte values big-endian. /// /// Sorts rows by timestamp first (spec §3.5). pub fn serialize_values(&mut self) -> Vec<u8> { @@ -212,7 +214,10 @@ impl Tablet { let nulls: Vec<bool> = col.iter().map(Option::is_none).collect(); if nulls.iter().any(|&n| n) { buf.push(1); - buf.extend_from_slice(&pack_bits_lsb_first(&nulls)); + // Pad to the server's fixed `rows/8 + 1` BitMap length. + let mut bitmap = pack_bits_lsb_first(&nulls); + bitmap.resize(rows / 8 + 1, 0); + buf.extend_from_slice(&bitmap); } else { buf.push(0); } @@ -432,19 +437,21 @@ mod tests { } #[test] - fn bitmap_is_ceil_rows_over_8() { - // 8 rows with a null: exactly 1 bitmap byte (ceil, not rows/8 + 1). + fn bitmap_is_rows_over_8_plus_1() { + // 8 rows with a null: the server's BitMap always reads rows/8 + 1 + // bytes, so an 8-row bitmap is 2 bytes (1 data + 1 padding). let mut t = tree_tablet(vec![TSDataType::Int32]); for i in 0..8 { let cell = if i == 7 { None } else { Some(Value::Int32(i)) }; t.add_row(i64::from(i), vec![cell]).unwrap(); } let buf = t.serialize_values(); - assert_eq!(buf.len(), 8 * 4 + 1 + 1); + assert_eq!(buf.len(), 8 * 4 + 1 + 2); assert_eq!(buf[8 * 4], 0x01); // flag assert_eq!(buf[8 * 4 + 1], 0x80); // row 7 null, LSB-first → bit 7 + assert_eq!(buf[8 * 4 + 2], 0x00); // padding byte - // 9 rows: 2 bitmap bytes. + // 9 rows: still 2 bitmap bytes (9/8 + 1 = 2). let mut t = tree_tablet(vec![TSDataType::Int32]); for i in 0..9 { let cell = if i == 8 { None } else { Some(Value::Int32(i)) }; diff --git a/src/error.rs b/src/error.rs index 6da4412..d28dbc8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,20 @@ +// 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. + //! Error types for the IoTDB client. use std::fmt; diff --git a/src/lib.rs b/src/lib.rs index 5cc766f..9b3d855 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,20 @@ +// 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. + //! Apache IoTDB client for Rust. //! //! Provides two data models over Apache Thrift RPC (default port 6667): diff --git a/src/protocol/client.rs b/src/protocol/client.rs index 8427fa5..1ec93a1 100644 --- a/src/protocol/client.rs +++ b/src/protocol/client.rs @@ -1,3 +1,20 @@ +// 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. + // Autogenerated by Thrift Compiler (0.14.1) // DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING diff --git a/src/protocol/common.rs b/src/protocol/common.rs index 70a29c5..fcbfc0a 100644 --- a/src/protocol/common.rs +++ b/src/protocol/common.rs @@ -1,3 +1,20 @@ +// 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. + // Autogenerated by Thrift Compiler (0.14.1) // DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index 13d8478..aa79bba 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -1,3 +1,20 @@ +// 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. + //! Generated Thrift RPC stubs. //! //! Source IDL lives in `thrift/`, synced from the IoTDB repo: diff --git a/tools/check-license.sh b/tools/check-license.sh new file mode 100755 index 0000000..50c514d --- /dev/null +++ b/tools/check-license.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# +# 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. +# + +# Checks that every source file carries the Apache 2.0 license header. +# Exits 1 listing the offenders, 0 when all files are compliant. + +set -euo pipefail +cd "$(dirname "$0")/.." + +MARKER="Licensed to the Apache Software Foundation" +offenders=() + +check() { + local f="$1" + # The header must appear near the top of the file (first 30 lines). + if ! head -30 "$f" | grep -q "$MARKER"; then + offenders+=("$f") + fi +} + +# Rust sources (including generated protocol stubs), examples and tests. +while IFS= read -r f; do check "$f"; done < <(find src examples tests -name '*.rs' 2>/dev/null) + +# Build / CI / infra files. +for f in Cargo.toml docker-compose*.yml .github/workflows/*.yml tools/*.sh thrift/*.thrift; do + [[ -f "$f" ]] && check "$f" +done + +if ((${#offenders[@]})); then + echo "Files missing the Apache 2.0 license header:" >&2 + printf ' %s\n' "${offenders[@]}" >&2 + exit 1 +fi +echo "License header check passed." diff --git a/tools/generate-thrift.sh b/tools/generate-thrift.sh index 9971046..ac441b7 100755 --- a/tools/generate-thrift.sh +++ b/tools/generate-thrift.sh @@ -64,4 +64,15 @@ fi # src/protocol/. Rewrite the cross-module path. sed -i '' 's/use crate::common;/use crate::protocol::common;/' src/protocol/client.rs +# Re-prepend the Apache license header (the generator emits bare files). +for f in src/protocol/common.rs src/protocol/client.rs; do + if ! head -30 "$f" | grep -q "Licensed to the Apache Software Foundation"; then + tmp=$(mktemp) + sed 's/^/\/\/ /; s/ *$//' tools/license-header.txt > "$tmp" + printf '\n' >> "$tmp" + cat "$f" >> "$tmp" + mv "$tmp" "$f" + fi +done + echo "Generated src/protocol/{common,client}.rs"
