blob: 177ece40770eb451511f9990551cd40687ea9fc8 [file]
// 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);
}
}