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.
+[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](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 客户端
+
+[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](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"

Reply via email to