| // 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. |
| |
| //! A batch of rows for one device (tree model) or one table (table model), |
| //! serialized column-major for `insertTablet` (protocol spec §3). |
| |
| use super::bitmap::pack_bits_lsb_first; |
| use super::value::Value; |
| use super::{ColumnCategory, TSDataType}; |
| use crate::error::{Error, Result}; |
| |
| /// A column-major batch of rows for `insertTablet`. |
| /// |
| /// Tree model: `target` is the device id (e.g. `root.sg.d1`) and |
| /// `column_categories` is `None`. Table model (via [`Tablet::new_table`]): |
| /// `target` is the table name and every column carries a |
| /// [`ColumnCategory`]. |
| /// |
| /// Rows are stably sorted by timestamp before serialization (spec §3.5); |
| /// the server fast path assumes sorted input. |
| #[derive(Debug, Clone)] |
| pub struct Tablet { |
| /// Device id (tree model) or table name (table model). |
| target: String, |
| /// Measurement names (tree model) or column names (table model). |
| measurements: Vec<String>, |
| types: Vec<TSDataType>, |
| timestamps: Vec<i64>, |
| /// Column-major: `values[col][row]`, `None` = null cell. |
| values: Vec<Vec<Option<Value>>>, |
| /// Table model only: one category per column. |
| column_categories: Option<Vec<ColumnCategory>>, |
| /// Tree model only: write to an aligned device. Always `false` on the |
| /// wire for table-model tablets (spec §6). |
| aligned: bool, |
| } |
| |
| impl Tablet { |
| /// Creates an empty tree-model tablet for `device_id`. |
| pub fn new( |
| device_id: impl Into<String>, |
| measurements: Vec<String>, |
| types: Vec<TSDataType>, |
| ) -> Result<Tablet> { |
| if measurements.len() != types.len() { |
| return Err(Error::Client(format!( |
| "measurement count ({}) != type count ({})", |
| measurements.len(), |
| types.len() |
| ))); |
| } |
| let columns = measurements.len(); |
| Ok(Tablet { |
| target: device_id.into(), |
| measurements, |
| types, |
| timestamps: Vec::new(), |
| values: vec![Vec::new(); columns], |
| column_categories: None, |
| aligned: false, |
| }) |
| } |
| |
| /// Creates an empty tree-model tablet for an **aligned** device. |
| pub fn new_aligned( |
| device_id: impl Into<String>, |
| measurements: Vec<String>, |
| types: Vec<TSDataType>, |
| ) -> Result<Tablet> { |
| let mut tablet = Tablet::new(device_id, measurements, types)?; |
| tablet.aligned = true; |
| Ok(tablet) |
| } |
| |
| /// Creates an empty table-model tablet for `table_name` with one |
| /// [`ColumnCategory`] per column. |
| pub fn new_table( |
| table_name: impl Into<String>, |
| column_names: Vec<String>, |
| types: Vec<TSDataType>, |
| column_categories: Vec<ColumnCategory>, |
| ) -> Result<Tablet> { |
| if column_categories.len() != types.len() { |
| return Err(Error::Client(format!( |
| "category count ({}) != type count ({})", |
| column_categories.len(), |
| types.len() |
| ))); |
| } |
| let mut tablet = Tablet::new(table_name, column_names, types)?; |
| tablet.column_categories = Some(column_categories); |
| Ok(tablet) |
| } |
| |
| /// Device id (tree model) or table name (table model). |
| pub fn target(&self) -> &str { |
| &self.target |
| } |
| |
| /// Table name — alias of [`Tablet::target`] for table-model tablets. |
| pub fn table_name(&self) -> &str { |
| &self.target |
| } |
| |
| pub fn measurements(&self) -> &[String] { |
| &self.measurements |
| } |
| |
| pub fn types(&self) -> &[TSDataType] { |
| &self.types |
| } |
| |
| pub fn timestamps(&self) -> &[i64] { |
| &self.timestamps |
| } |
| |
| /// Mutable access to the timestamp column, e.g. to rebase a reused |
| /// tablet onto a fresh time range without regenerating its values. |
| /// Rows are re-sorted on serialization, so callers need not preserve |
| /// ordering. |
| pub fn timestamps_mut(&mut self) -> &mut [i64] { |
| &mut self.timestamps |
| } |
| |
| /// `Some` iff this is a table-model tablet. |
| pub fn column_categories(&self) -> Option<&[ColumnCategory]> { |
| self.column_categories.as_deref() |
| } |
| |
| /// True for aligned-device tree-model tablets (spec §3.1 field 8). |
| /// Always `false` for table-model tablets. |
| pub fn is_aligned(&self) -> bool { |
| self.aligned |
| } |
| |
| pub fn row_count(&self) -> usize { |
| self.timestamps.len() |
| } |
| |
| pub fn is_table_model(&self) -> bool { |
| self.column_categories.is_some() |
| } |
| |
| /// Appends one row. `row[i]` must be `None` (null) or a [`Value`] whose |
| /// type matches `types[i]`. |
| pub fn add_row(&mut self, timestamp: i64, row: Vec<Option<Value>>) -> Result<()> { |
| if row.len() != self.types.len() { |
| return Err(Error::Client(format!( |
| "row has {} cells, tablet has {} columns", |
| row.len(), |
| self.types.len() |
| ))); |
| } |
| for (i, cell) in row.iter().enumerate() { |
| match cell { |
| None | Some(Value::Null) => {} |
| Some(v) if v.data_type() == Some(self.types[i]) => {} |
| Some(v) => { |
| return Err(Error::Client(format!( |
| "column {i} ({}) expects {:?}, got {v:?}", |
| self.measurements[i], self.types[i] |
| ))); |
| } |
| } |
| } |
| self.timestamps.push(timestamp); |
| for (col, cell) in self.values.iter_mut().zip(row) { |
| // Normalize Some(Null) to None so null detection is uniform. |
| col.push(cell.filter(|v| !v.is_null())); |
| } |
| Ok(()) |
| } |
| |
| /// Stably sorts rows by timestamp, reordering all value columns in step. |
| pub fn sort_by_timestamp(&mut self) { |
| let n = self.timestamps.len(); |
| let mut order: Vec<usize> = (0..n).collect(); |
| order.sort_by_key(|&i| self.timestamps[i]); // stable |
| if order.iter().enumerate().all(|(pos, &i)| pos == i) { |
| return; |
| } |
| self.timestamps = order.iter().map(|&i| self.timestamps[i]).collect(); |
| for col in &mut self.values { |
| *col = order.iter().map(|&i| col[i].clone()).collect(); |
| } |
| } |
| |
| /// 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 `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> { |
| self.sort_by_timestamp(); |
| let rows = self.row_count(); |
| let mut buf = Vec::new(); |
| for (col, &ty) in self.values.iter().zip(&self.types) { |
| for cell in col { |
| write_cell(&mut buf, ty, cell.as_ref()); |
| } |
| } |
| // Trailing per-column null bitmap section. |
| for col in &self.values { |
| let nulls: Vec<bool> = col.iter().map(Option::is_none).collect(); |
| if nulls.iter().any(|&n| n) { |
| buf.push(1); |
| // 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); |
| } |
| } |
| debug_assert!(self.values.iter().all(|c| c.len() == rows)); |
| buf |
| } |
| |
| /// Serializes the timestamp buffer per spec §3.4: `row_count` contiguous |
| /// i64 big-endian values, no count prefix. |
| /// |
| /// Sorts rows by timestamp first (spec §3.5). |
| pub fn serialize_timestamps(&mut self) -> Vec<u8> { |
| self.sort_by_timestamp(); |
| let mut buf = Vec::with_capacity(self.timestamps.len() * 8); |
| for ts in &self.timestamps { |
| buf.extend_from_slice(&ts.to_be_bytes()); |
| } |
| buf |
| } |
| } |
| |
| /// Writes one cell, using the C#-style sentinel placeholders for nulls |
| /// (spec §3.2 — any placeholder works, the server masks by bitmap). |
| fn write_cell(buf: &mut Vec<u8>, ty: TSDataType, cell: Option<&Value>) { |
| match (ty, cell) { |
| (TSDataType::Boolean, Some(Value::Boolean(b))) => buf.push(u8::from(*b)), |
| (TSDataType::Boolean, None) => buf.push(0), |
| (TSDataType::Int32, Some(Value::Int32(v))) => buf.extend_from_slice(&v.to_be_bytes()), |
| (TSDataType::Int32, None) => buf.extend_from_slice(&i32::MIN.to_be_bytes()), |
| (TSDataType::Int64, Some(Value::Int64(v))) => buf.extend_from_slice(&v.to_be_bytes()), |
| (TSDataType::Timestamp, Some(Value::Timestamp(v))) => { |
| buf.extend_from_slice(&v.to_be_bytes()) |
| } |
| (TSDataType::Int64 | TSDataType::Timestamp, None) => { |
| buf.extend_from_slice(&i64::MIN.to_be_bytes()) |
| } |
| (TSDataType::Float, Some(Value::Float(v))) => buf.extend_from_slice(&v.to_be_bytes()), |
| (TSDataType::Float, None) => buf.extend_from_slice(&f32::MIN.to_be_bytes()), |
| (TSDataType::Double, Some(Value::Double(v))) => buf.extend_from_slice(&v.to_be_bytes()), |
| (TSDataType::Double, None) => buf.extend_from_slice(&f64::MIN.to_be_bytes()), |
| (TSDataType::Text, Some(Value::Text(s))) | (TSDataType::String, Some(Value::String(s))) => { |
| write_binary(buf, s.as_bytes()); |
| } |
| (TSDataType::Blob, Some(Value::Blob(b))) => write_binary(buf, b), |
| (TSDataType::Text | TSDataType::String | TSDataType::Blob, None) => write_binary(buf, &[]), |
| // Null sentinel 10000101 = 1000-01-01 (yyyyMMdd), per C#/Java. |
| (TSDataType::Date, Some(Value::Date(v))) => buf.extend_from_slice(&v.to_be_bytes()), |
| (TSDataType::Date, None) => buf.extend_from_slice(&10000101i32.to_be_bytes()), |
| // add_row validates cell types; Vector/Unknown are not insertable. |
| (ty, cell) => unreachable!("cell {cell:?} does not match tablet column type {ty:?}"), |
| } |
| } |
| |
| /// 4-byte big-endian length prefix + raw bytes (TEXT/STRING/BLOB, §3.2). |
| /// Shared with the record path (`data::record`), which prefixes the same |
| /// encoding with a per-cell type marker. |
| pub(super) fn write_binary(buf: &mut Vec<u8>, bytes: &[u8]) { |
| buf.extend_from_slice(&(bytes.len() as i32).to_be_bytes()); |
| buf.extend_from_slice(bytes); |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| fn tree_tablet(types: Vec<TSDataType>) -> Tablet { |
| let measurements = (0..types.len()).map(|i| format!("s{i}")).collect(); |
| Tablet::new("root.sg.d1", measurements, types).unwrap() |
| } |
| |
| #[test] |
| fn int32_column_with_null_known_bytes() { |
| let mut t = tree_tablet(vec![TSDataType::Int32]); |
| t.add_row(1, vec![Some(Value::Int32(5))]).unwrap(); |
| t.add_row(2, vec![None]).unwrap(); |
| let expected = [ |
| 0x00, 0x00, 0x00, 0x05, // row 0: 5 |
| 0x80, 0x00, 0x00, 0x00, // row 1: i32::MIN placeholder |
| 0x01, // bitmap flag: has nulls |
| 0x02, // LSB-first: row 1 null → bit 1 → 0x02 |
| ]; |
| assert_eq!(t.serialize_values(), expected); |
| assert_eq!( |
| t.serialize_timestamps(), |
| [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2] |
| ); |
| } |
| |
| #[test] |
| fn no_nulls_writes_zero_flag_only() { |
| let mut t = tree_tablet(vec![TSDataType::Boolean]); |
| t.add_row(1, vec![Some(Value::Boolean(true))]).unwrap(); |
| t.add_row(2, vec![Some(Value::Boolean(false))]).unwrap(); |
| assert_eq!(t.serialize_values(), [0x01, 0x00, 0x00]); |
| } |
| |
| #[test] |
| fn all_types_known_bytes() { |
| let types = vec![ |
| TSDataType::Boolean, |
| TSDataType::Int32, |
| TSDataType::Int64, |
| TSDataType::Float, |
| TSDataType::Double, |
| TSDataType::Text, |
| TSDataType::Timestamp, |
| TSDataType::Date, |
| TSDataType::Blob, |
| TSDataType::String, |
| ]; |
| let mut t = tree_tablet(types); |
| t.add_row( |
| 100, |
| vec![ |
| Some(Value::Boolean(true)), |
| Some(Value::Int32(7)), |
| Some(Value::Int64(-2)), |
| Some(Value::Float(1.5)), |
| Some(Value::Double(-0.5)), |
| Some(Value::Text("ab".into())), |
| Some(Value::Timestamp(3)), |
| Some(Value::Date(20260710)), |
| Some(Value::Blob(vec![0xDE, 0xAD])), |
| Some(Value::String("é".into())), |
| ], |
| ) |
| .unwrap(); |
| |
| let mut expected: Vec<u8> = Vec::new(); |
| expected.push(0x01); // true |
| expected.extend_from_slice(&7i32.to_be_bytes()); |
| expected.extend_from_slice(&(-2i64).to_be_bytes()); |
| expected.extend_from_slice(&[0x3F, 0xC0, 0x00, 0x00]); // 1.5f32 IEEE 754 BE |
| expected.extend_from_slice(&(-0.5f64).to_be_bytes()); |
| expected.extend_from_slice(&[0, 0, 0, 2, b'a', b'b']); |
| expected.extend_from_slice(&3i64.to_be_bytes()); |
| expected.extend_from_slice(&20260710i32.to_be_bytes()); |
| expected.extend_from_slice(&[0, 0, 0, 2, 0xDE, 0xAD]); |
| expected.extend_from_slice(&[0, 0, 0, 2, 0xC3, 0xA9]); // "é" UTF-8 |
| expected.extend_from_slice(&[0; 10]); // 10 columns, no nulls |
| assert_eq!(t.serialize_values(), expected); |
| } |
| |
| #[test] |
| fn all_types_null_placeholders() { |
| let types = vec![ |
| TSDataType::Boolean, |
| TSDataType::Int32, |
| TSDataType::Int64, |
| TSDataType::Float, |
| TSDataType::Double, |
| TSDataType::Text, |
| TSDataType::Timestamp, |
| TSDataType::Date, |
| TSDataType::Blob, |
| TSDataType::String, |
| ]; |
| let n = types.len(); |
| let mut t = tree_tablet(types); |
| t.add_row(1, vec![None; n]).unwrap(); |
| |
| let mut expected: Vec<u8> = Vec::new(); |
| expected.push(0x00); // false |
| expected.extend_from_slice(&i32::MIN.to_be_bytes()); |
| expected.extend_from_slice(&i64::MIN.to_be_bytes()); |
| expected.extend_from_slice(&f32::MIN.to_be_bytes()); |
| expected.extend_from_slice(&f64::MIN.to_be_bytes()); |
| expected.extend_from_slice(&[0, 0, 0, 0]); // empty text |
| expected.extend_from_slice(&i64::MIN.to_be_bytes()); |
| expected.extend_from_slice(&10000101i32.to_be_bytes()); // 1000-01-01 |
| expected.extend_from_slice(&[0, 0, 0, 0]); // empty blob |
| expected.extend_from_slice(&[0, 0, 0, 0]); // empty string |
| for _ in 0..n { |
| expected.extend_from_slice(&[0x01, 0x01]); // flag + bitmap (row 0 null) |
| } |
| assert_eq!(t.serialize_values(), expected); |
| } |
| |
| #[test] |
| fn unsorted_rows_are_sorted_by_timestamp() { |
| let mut t = tree_tablet(vec![TSDataType::Int32]); |
| t.add_row(3, vec![Some(Value::Int32(30))]).unwrap(); |
| t.add_row(1, vec![None]).unwrap(); |
| t.add_row(2, vec![Some(Value::Int32(20))]).unwrap(); |
| |
| let ts = t.serialize_timestamps(); |
| assert_eq!( |
| ts, |
| [1i64, 2, 3] |
| .iter() |
| .flat_map(|v| v.to_be_bytes()) |
| .collect::<Vec<u8>>() |
| ); |
| // Values reordered with their rows; the null moved to row 0. |
| let expected = [ |
| 0x80, 0x00, 0x00, 0x00, // null placeholder (was ts=1) |
| 0x00, 0x00, 0x00, 0x14, // 20 (ts=2) |
| 0x00, 0x00, 0x00, 0x1E, // 30 (ts=3) |
| 0x01, 0x01, // flag + LSB-first bitmap: row 0 null |
| ]; |
| assert_eq!(t.serialize_values(), expected); |
| assert_eq!(t.timestamps(), &[1, 2, 3]); |
| } |
| |
| #[test] |
| fn sort_is_stable_for_equal_timestamps() { |
| let mut t = tree_tablet(vec![TSDataType::Text]); |
| t.add_row(5, vec![Some(Value::Text("first".into()))]) |
| .unwrap(); |
| t.add_row(1, vec![Some(Value::Text("zero".into()))]) |
| .unwrap(); |
| t.add_row(5, vec![Some(Value::Text("second".into()))]) |
| .unwrap(); |
| t.sort_by_timestamp(); |
| assert_eq!(t.timestamps(), &[1, 5, 5]); |
| assert_eq!(t.values[0][1], Some(Value::Text("first".into()))); |
| assert_eq!(t.values[0][2], Some(Value::Text("second".into()))); |
| } |
| |
| #[test] |
| 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 + 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: 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)) }; |
| t.add_row(i64::from(i), vec![cell]).unwrap(); |
| } |
| let buf = t.serialize_values(); |
| assert_eq!(buf.len(), 9 * 4 + 1 + 2); |
| assert_eq!(&buf[9 * 4..], [0x01, 0x00, 0x01]); // row 8 → byte 1 bit 0 |
| } |
| |
| #[test] |
| fn multi_column_is_column_major() { |
| let mut t = tree_tablet(vec![TSDataType::Int32, TSDataType::Boolean]); |
| t.add_row(1, vec![Some(Value::Int32(1)), Some(Value::Boolean(true))]) |
| .unwrap(); |
| t.add_row(2, vec![Some(Value::Int32(2)), Some(Value::Boolean(false))]) |
| .unwrap(); |
| let expected = [ |
| 0, 0, 0, 1, 0, 0, 0, 2, // int32 column, both rows |
| 1, 0, // boolean column, both rows |
| 0, 0, // both flags: no nulls |
| ]; |
| assert_eq!(t.serialize_values(), expected); |
| } |
| |
| #[test] |
| fn empty_tablet_serializes_flags_only() { |
| let mut t = tree_tablet(vec![TSDataType::Int64]); |
| assert_eq!(t.serialize_values(), [0x00]); |
| assert_eq!(t.serialize_timestamps(), Vec::<u8>::new()); |
| } |
| |
| #[test] |
| fn table_model_tablet() { |
| let t = Tablet::new_table( |
| "sensors", |
| vec!["tag1".into(), "f1".into(), "attr1".into()], |
| vec![TSDataType::String, TSDataType::Double, TSDataType::String], |
| vec![ |
| ColumnCategory::Tag, |
| ColumnCategory::Field, |
| ColumnCategory::Attribute, |
| ], |
| ) |
| .unwrap(); |
| assert!(t.is_table_model()); |
| assert_eq!(t.table_name(), "sensors"); |
| assert_eq!( |
| t.column_categories().unwrap(), |
| &[ |
| ColumnCategory::Tag, |
| ColumnCategory::Field, |
| ColumnCategory::Attribute |
| ] |
| ); |
| } |
| |
| #[test] |
| fn constructor_and_add_row_validation() { |
| assert!(Tablet::new("d", vec!["s1".into()], vec![]).is_err()); |
| assert!(Tablet::new_table( |
| "t", |
| vec!["c1".into()], |
| vec![TSDataType::Int32], |
| vec![ColumnCategory::Tag, ColumnCategory::Field], |
| ) |
| .is_err()); |
| |
| let mut t = tree_tablet(vec![TSDataType::Int32]); |
| // Wrong arity. |
| assert!(t.add_row(1, vec![]).is_err()); |
| // Wrong type. |
| assert!(t.add_row(1, vec![Some(Value::Int64(1))]).is_err()); |
| // Explicit Value::Null behaves like None. |
| t.add_row(1, vec![Some(Value::Null)]).unwrap(); |
| assert_eq!(&t.serialize_values()[4..], [0x01, 0x01]); // flag + null bitmap |
| assert_eq!(t.row_count(), 1); |
| } |
| } |