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 f5be5860721c4fadb86bddbc524ef99c950a3950 Author: CritasWang <[email protected]> AuthorDate: Mon Jul 13 10:11:28 2026 +0800 V1: DATE live adjudication test; re-tag logical types (DATE/TIMESTAMP/STRING/BLOB) from dataTypeList in row assembly --- src/client/dataset.rs | 60 ++++++++++++++++++++++++++++++++++++- src/client/session.rs | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 1 deletion(-) diff --git a/src/client/dataset.rs b/src/client/dataset.rs index 457b213..6c18ff5 100644 --- a/src/client/dataset.rs +++ b/src/client/dataset.rs @@ -173,12 +173,29 @@ impl<'a> SessionDataSet<'a> { block.columns.len() )) })?; - values.push(column[i].clone()); + values.push(Self::apply_logical_type( + column[i].clone(), + self.data_type_list.get(ordinal).map(String::as_str), + )); } let timestamp = (!self.ignore_time_stamp).then(|| block.timestamps[i]); Ok(Row { timestamp, values }) } + /// Re-tag a decoded value with the column's logical type from the + /// response's `dataTypeList`. TsBlock headers carry the *physical* type + /// (DATE arrives as INT32, TIMESTAMP as INT64, STRING as TEXT), so the + /// block decoder alone cannot distinguish them. + fn apply_logical_type(value: Value, logical: Option<&str>) -> Value { + match (logical, value) { + (Some("DATE"), Value::Int32(v)) => Value::Date(v), + (Some("TIMESTAMP"), Value::Int64(v)) => Value::Timestamp(v), + (Some("STRING"), Value::Text(s)) => Value::String(s), + (Some("BLOB"), Value::Text(s)) => Value::Blob(s.into_bytes()), + (_, v) => v, + } + } + /// Close the query (best-effort `closeOperation`, errors swallowed) and /// release the session borrow's obligations. Idempotent; also invoked /// automatically on exhaustion and on drop. @@ -226,6 +243,47 @@ mod tests { } } + #[test] + fn logical_type_retagging_from_data_type_list() { + // DATE arrives physically as INT32 in the TsBlock; dataTypeList says DATE. + let mut session = offline_session(); + let mut h = handle(vec![int32_block(&[1], &[20260713])], false); + h.data_type_list = vec!["DATE".into()]; + let mut ds = SessionDataSet::new(&mut session, h); + let row = ds.next_row().unwrap().unwrap(); + assert_eq!(row.values[0], Value::Date(20260713)); + assert!(ds.next_row().unwrap().is_none()); + } + + #[test] + fn apply_logical_type_variants() { + use Value::*; + // Physical → logical re-tags. + assert_eq!( + SessionDataSet::apply_logical_type(Int32(20260713), Some("DATE")), + Date(20260713) + ); + assert_eq!( + SessionDataSet::apply_logical_type(Int64(99), Some("TIMESTAMP")), + Timestamp(99) + ); + assert_eq!( + SessionDataSet::apply_logical_type(Text("s".into()), Some("STRING")), + String("s".into()) + ); + assert_eq!( + SessionDataSet::apply_logical_type(Text("b".into()), Some("BLOB")), + Blob(b"b".to_vec()) + ); + // Pass-throughs: matching physical types and nulls stay untouched. + assert_eq!( + SessionDataSet::apply_logical_type(Int32(5), Some("INT32")), + Int32(5) + ); + assert_eq!(SessionDataSet::apply_logical_type(Null, Some("DATE")), Null); + assert_eq!(SessionDataSet::apply_logical_type(Int32(5), None), Int32(5)); + } + #[test] fn drains_rows_across_multiple_cached_blocks() { let mut session = offline_session(); diff --git a/src/client/session.rs b/src/client/session.rs index 775391b..f84ad43 100644 --- a/src/client/session.rs +++ b/src/client/session.rs @@ -542,6 +542,88 @@ mod tests { assert!(!session.is_open()); } + /// DATE wire-format adjudication test (goal V1). Inserts one DATE row via + /// the tablet binary path (i32 yyyyMMdd) and one via a SQL date literal + /// (parsed server-side), then reads both back: if the tablet encoding is + /// correct, both rows decode to the same i32 for the same calendar date. + /// This breaks the write-read circularity a plain roundtrip would have. + /// Skipped when no IoTDB instance is reachable on localhost:6667. + #[test] + fn live_date_encoding_adjudication() { + use crate::data::{tablet::Tablet, TSDataType, Value}; + use std::net::TcpStream; + if TcpStream::connect_timeout( + &"127.0.0.1:6667".parse().unwrap(), + Duration::from_millis(300), + ) + .is_err() + { + eprintln!( + "skipping live_date_encoding_adjudication: no IoTDB server on 127.0.0.1:6667" + ); + return; + } + + const DB: &str = "root.rusttest_date"; + // 2026-07-13 as yyyyMMdd; deliberately not near epoch so a + // days-since-epoch misinterpretation cannot coincide. + const DATE_YYYYMMDD: i32 = 20260713; + + let mut session = Session::new(SessionConfig::default()); + session.open().expect("open session"); + let _ = session.execute_non_query(&format!("DELETE DATABASE {DB}")); + session + .execute_non_query(&format!("CREATE DATABASE {DB}")) + .expect("create database"); + + // Row at ts=1: tablet binary path. + let mut tablet = Tablet::new( + format!("{DB}.d1"), + vec!["dt".into()], + vec![TSDataType::Date], + ) + .expect("tablet"); + tablet + .add_row(1, vec![Some(Value::Date(DATE_YYYYMMDD))]) + .expect("add_row"); + session.insert_tablet(&tablet).expect("insert_tablet"); + + // Row at ts=2: SQL literal path — server parses the calendar date itself. + session + .execute_non_query(&format!( + "INSERT INTO {DB}.d1(timestamp, dt) VALUES (2, '2026-07-13')" + )) + .expect("insert via SQL literal"); + + // Read both back; they must decode identically. + let mut got: Vec<(i64, Value)> = Vec::new(); + { + let mut dataset = session + .execute_query(&format!("SELECT dt FROM {DB}.d1 ORDER BY time")) + .expect("query"); + while let Some(row) = dataset.next_row().expect("next_row") { + got.push((row.timestamp.expect("timestamp"), row.values[0].clone())); + } + } + assert_eq!(got.len(), 2, "expected both rows back"); + assert_eq!( + got[0].1, + Value::Date(DATE_YYYYMMDD), + "tablet-path DATE readback" + ); + assert_eq!( + got[1].1, + Value::Date(DATE_YYYYMMDD), + "SQL-literal DATE must decode to the same i32 as the tablet path — \ + proves yyyyMMdd is the server's wire semantics" + ); + + session + .execute_non_query(&format!("DELETE DATABASE {DB}")) + .expect("cleanup"); + session.close().expect("close session"); + } + /// Value-asserting live roundtrip: unsorted input, nulls, and a row count /// that is a multiple of 8 (stresses the rows/8+1 bitmap padding byte). /// Skipped when no IoTDB instance is reachable on localhost:6667.
