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 6cefccc9575adfa8df38ad8e1430a065eff70e86 Author: CritasWang <[email protected]> AuthorDate: Tue Jul 14 16:44:44 2026 +0800 Fix BLOB readback (physical TEXT column with non-UTF-8 payload kept as Blob for logical re-tag); add verify_write example (2 TAG + 2 ATTRIBUTE + all 10 FIELD types, nulls per row) --- examples/verify_write.rs | 236 +++++++++++++++++++++++++++++++++++++++++++++++ src/data/tsblock.rs | 9 +- 2 files changed, 244 insertions(+), 1 deletion(-) diff --git a/examples/verify_write.rs b/examples/verify_write.rs new file mode 100644 index 0000000..40be7dd --- /dev/null +++ b/examples/verify_write.rs @@ -0,0 +1,236 @@ +// 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. + +//! Manual data-correctness check: write 10 rows covering every TSDataType +//! (one FIELD per type, plus 2 TAGs + 2 ATTRIBUTEs on the table model), +//! every row containing at least one null, every value distinct — then read +//! everything back and print it for eyeball comparison. +//! +//! Usage: cargo run --example verify_write -- <host> <port> <user> <password> + +use iotdb_client::{ + ColumnCategory, Result, Session, SessionConfig, TSDataType, TableSession, Tablet, Value, +}; + +const TREE_DB: &str = "root.verify_rust"; +const TABLE_DB: &str = "verify_rust"; + +/// FIELD columns: one per TSDataType (10 types). +const FIELD_DEFS: [(&str, TSDataType); 10] = [ + ("f_boolean", TSDataType::Boolean), + ("f_int32", TSDataType::Int32), + ("f_int64", TSDataType::Int64), + ("f_float", TSDataType::Float), + ("f_double", TSDataType::Double), + ("f_text", TSDataType::Text), + ("f_timestamp", TSDataType::Timestamp), + ("f_date", TSDataType::Date), + ("f_blob", TSDataType::Blob), + ("f_string", TSDataType::String), +]; + +/// Value for field column `col` at row `r` (all distinct across rows), or +/// None on the anti-diagonal so every row has exactly one null field: +/// row r nulls field (10 - 1 - r). +fn field_value(col: usize, r: usize) -> Option<Value> { + if col == 9 - r { + return None; + } + let i = r as i32; + Some(match col { + 0 => Value::Boolean(r % 2 == 0), + 1 => Value::Int32(100 + i), + 2 => Value::Int64(1_000_000_000_000 + i as i64), + 3 => Value::Float(1.5 + i as f32 * 0.25), + 4 => Value::Double(2.25 + i as f64 * 0.5), + 5 => Value::Text(format!("text-{r:02}")), + 6 => Value::Timestamp(1_720_000_000_000 + i as i64 * 1000), + 7 => Value::Date(20260701 + i), // 2026-07-01 .. 2026-07-10 + 8 => Value::Blob(vec![0xB0 + r as u8, 0x00, 0xFF - r as u8]), + 9 => Value::String(format!("str-{r:02}")), + _ => unreachable!(), + }) +} + +fn print_resultset(mut ds: iotdb_client::SessionDataSet<'_>) -> Result<usize> { + let headers = ds.columns().join(" | "); + println!(" {headers}"); + let mut n = 0; + while let Some(row) = ds.next_row()? { + let ts = row + .timestamp + .map(|t| t.to_string()) + .unwrap_or_else(|| "-".into()); + let cells: Vec<String> = row + .values + .iter() + .map(|v| match v { + Value::Null => "null".to_string(), + Value::Blob(b) => format!( + "0x{}", + b.iter().map(|x| format!("{x:02X}")).collect::<String>() + ), + other => format!("{other:?}"), + }) + .collect(); + println!(" ts={ts} | {}", cells.join(" | ")); + n += 1; + } + Ok(n) +} + +fn tree_model(host: &str, port: u16, user: &str, password: &str) -> Result<()> { + println!("\n=== TREE MODEL ({TREE_DB}) ==="); + let mut session = Session::new(SessionConfig { + endpoints: vec![iotdb_client::Endpoint::new(host, port)], + username: user.into(), + password: password.into(), + ..Default::default() + }); + session.open()?; + let _ = session.execute_non_query(&format!("DELETE DATABASE {TREE_DB}")); + session.execute_non_query(&format!("CREATE DATABASE {TREE_DB}"))?; + for (name, ty) in FIELD_DEFS { + session.execute_non_query(&format!( + "CREATE TIMESERIES {TREE_DB}.d1.{name} WITH datatype={}", + match ty { + TSDataType::Boolean => "BOOLEAN", + TSDataType::Int32 => "INT32", + TSDataType::Int64 => "INT64", + TSDataType::Float => "FLOAT", + TSDataType::Double => "DOUBLE", + TSDataType::Text => "TEXT", + TSDataType::Timestamp => "TIMESTAMP", + TSDataType::Date => "DATE", + TSDataType::Blob => "BLOB", + TSDataType::String => "STRING", + _ => unreachable!(), + } + ))?; + } + + let mut tablet = Tablet::new( + format!("{TREE_DB}.d1"), + FIELD_DEFS.iter().map(|(n, _)| n.to_string()).collect(), + FIELD_DEFS.iter().map(|&(_, t)| t).collect(), + )?; + for r in 0..10 { + tablet.add_row( + 1000 + r as i64, + (0..10).map(|c| field_value(c, r)).collect(), + )?; + } + session.insert_tablet(&tablet)?; + + println!("-- read back: SELECT * FROM {TREE_DB}.d1 --"); + let n = print_resultset(session.execute_query(&format!("SELECT * FROM {TREE_DB}.d1"))?)?; + println!("-- {n} rows --"); + session.close()?; + Ok(()) +} + +fn table_model(host: &str, port: u16, user: &str, password: &str) -> Result<()> { + println!("\n=== TABLE MODEL (db {TABLE_DB}, table sensor_data) ==="); + let mut session = TableSession::builder() + .node_urls(&[format!("{host}:{port}")])? + .username(user) + .password(password) + .build()?; + let _ = session.execute_non_query(&format!("DROP DATABASE {TABLE_DB}")); + session.execute_non_query(&format!("CREATE DATABASE {TABLE_DB}"))?; + session.execute_non_query(&format!("USE {TABLE_DB}"))?; + + let mut ddl = String::from( + "CREATE TABLE sensor_data (region STRING TAG, device_id STRING TAG, \ + vendor STRING ATTRIBUTE, model STRING ATTRIBUTE", + ); + for (name, ty) in FIELD_DEFS { + ddl.push_str(&format!( + ", {name} {} FIELD", + match ty { + TSDataType::Boolean => "BOOLEAN", + TSDataType::Int32 => "INT32", + TSDataType::Int64 => "INT64", + TSDataType::Float => "FLOAT", + TSDataType::Double => "DOUBLE", + TSDataType::Text => "TEXT", + TSDataType::Timestamp => "TIMESTAMP", + TSDataType::Date => "DATE", + TSDataType::Blob => "BLOB", + TSDataType::String => "STRING", + _ => unreachable!(), + } + )); + } + ddl.push(')'); + session.execute_non_query(&ddl)?; + + let mut names = vec![ + "region".to_string(), + "device_id".to_string(), + "vendor".to_string(), + "model".to_string(), + ]; + let mut types = vec![TSDataType::String; 4]; + let mut cats = vec![ + ColumnCategory::Tag, + ColumnCategory::Tag, + ColumnCategory::Attribute, + ColumnCategory::Attribute, + ]; + for (name, ty) in FIELD_DEFS { + names.push(name.to_string()); + types.push(ty); + cats.push(ColumnCategory::Field); + } + let mut tablet = Tablet::new_table("sensor_data", names, types, cats)?; + for r in 0..10 { + // Tags/attributes vary too: two regions, ten devices, two vendors. + let mut row: Vec<Option<Value>> = vec![ + Some(Value::String(format!("region-{}", r % 2))), + Some(Value::String(format!("dev-{r:02}"))), + Some(Value::String(format!( + "vendor-{}", + if r < 5 { "A" } else { "B" } + ))), + Some(Value::String(format!("model-{}", 100 + r))), + ]; + row.extend((0..10).map(|c| field_value(c, r))); + tablet.add_row(1000 + r as i64, row)?; + } + session.insert(&tablet)?; + + println!("-- read back: SELECT * FROM sensor_data ORDER BY device_id --"); + let n = + print_resultset(session.execute_query("SELECT * FROM sensor_data ORDER BY device_id")?)?; + println!("-- {n} rows --"); + session.close()?; + Ok(()) +} + +fn main() -> Result<()> { + let args: Vec<String> = std::env::args().skip(1).collect(); + let host = args.first().map(String::as_str).unwrap_or("127.0.0.1"); + let port: u16 = args.get(1).and_then(|p| p.parse().ok()).unwrap_or(6667); + let user = args.get(2).map(String::as_str).unwrap_or("root"); + let password = args.get(3).map(String::as_str).unwrap_or("root"); + + tree_model(host, port, user, password)?; + table_model(host, port, user, password)?; + println!("\nDone. Databases {TREE_DB} (tree) and {TABLE_DB} (table) kept for inspection."); + Ok(()) +} diff --git a/src/data/tsblock.rs b/src/data/tsblock.rs index f9265e8..40cd26a 100644 --- a/src/data/tsblock.rs +++ b/src/data/tsblock.rs @@ -213,7 +213,14 @@ fn decode_column( let bytes = r.read_bytes(len as usize)?.to_vec(); out.push(match ty { TSDataType::Blob => Value::Blob(bytes), - TSDataType::Text => Value::Text(decode_utf8(bytes)?), + // TsBlock headers carry the *physical* type: BLOB columns + // arrive typed TEXT here. Keep non-UTF-8 payloads as Blob + // so row assembly can re-tag them via the response's + // logical dataTypeList (same pattern as DATE/INT32). + TSDataType::Text => match String::from_utf8(bytes) { + Ok(s) => Value::Text(s), + Err(e) => Value::Blob(e.into_bytes()), + }, TSDataType::String => Value::String(decode_utf8(bytes)?), _ => { return Err(Error::Decode(format!(
