blob: 3f661f573cd6a586b703d98c2b3548434ef74bb0 [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 dynamically-typed IoTDB cell value.
use super::TSDataType;
use crate::error::{Error, Result};
/// One cell of an IoTDB row: a typed scalar or `Null`.
///
/// `Date` carries an `i32` in `yyyyMMdd` form (e.g. 2026-07-10 →
/// `20260710`), matching the C#/Java wire encoding. `Timestamp` is epoch
/// milliseconds.
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Boolean(bool),
Int32(i32),
Int64(i64),
Float(f32),
Double(f64),
Text(String),
Timestamp(i64),
/// Date as i32 `yyyyMMdd` (e.g. `20260710`).
Date(i32),
Blob(Vec<u8>),
String(String),
/// Table-model OBJECT cell. The bytes are the **already-framed OBJECT
/// segment**: `[1 byte isEOF][8 byte big-endian offset][content]`
/// (see `Tablet::build_object_value` / `Tablet::set_object_value_at`).
Object(Vec<u8>),
Null,
}
impl Value {
/// The [`TSDataType`] this value carries, or `None` for [`Value::Null`].
pub fn data_type(&self) -> Option<TSDataType> {
Some(match self {
Value::Boolean(_) => TSDataType::Boolean,
Value::Int32(_) => TSDataType::Int32,
Value::Int64(_) => TSDataType::Int64,
Value::Float(_) => TSDataType::Float,
Value::Double(_) => TSDataType::Double,
Value::Text(_) => TSDataType::Text,
Value::Timestamp(_) => TSDataType::Timestamp,
Value::Date(_) => TSDataType::Date,
Value::Blob(_) => TSDataType::Blob,
Value::String(_) => TSDataType::String,
Value::Object(_) => TSDataType::Object,
Value::Null => return None,
})
}
/// The wire type code (§8 of the protocol spec), or `None` for `Null`.
pub fn type_code(&self) -> Option<i32> {
self.data_type().map(TSDataType::code)
}
/// True iff this is [`Value::Null`].
pub fn is_null(&self) -> bool {
matches!(self, Value::Null)
}
}
/// Formats the wire representation of a stored OBJECT value for display.
///
/// The server stores OBJECT cells as an 8-byte big-endian file size followed
/// by the internal object path. Mirroring the Go client's
/// `objectBytesToString` and the Node/C# helpers, this renders the size as
/// `(Object) 1023 B` / `(Object) 1.00 KB` / `(Object) 1.00 MB` /
/// `(Object) 1.00 GB`.
pub fn object_bytes_to_string(bytes: &[u8]) -> Result<String> {
if bytes.len() < 8 {
return Err(Error::Decode(format!(
"invalid OBJECT value: expected at least 8 bytes, got {}",
bytes.len()
)));
}
let mut size_bytes = [0u8; 8];
size_bytes.copy_from_slice(&bytes[..8]);
let size = u64::from_be_bytes(size_bytes);
const KILOBYTE: f64 = 1024.0;
const MEGABYTE: f64 = KILOBYTE * 1024.0;
const GIGABYTE: f64 = MEGABYTE * 1024.0;
let size = size as f64;
if size < KILOBYTE {
Ok(format!("(Object) {size:.0} B"))
} else if size < MEGABYTE {
Ok(format!("(Object) {:.2} KB", size / KILOBYTE))
} else if size < GIGABYTE {
Ok(format!("(Object) {:.2} MB", size / MEGABYTE))
} else {
Ok(format!("(Object) {:.2} GB", size / GIGABYTE))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn type_codes() {
assert_eq!(Value::Boolean(true).type_code(), Some(0));
assert_eq!(Value::Int32(1).type_code(), Some(1));
assert_eq!(Value::Int64(1).type_code(), Some(2));
assert_eq!(Value::Float(1.0).type_code(), Some(3));
assert_eq!(Value::Double(1.0).type_code(), Some(4));
assert_eq!(Value::Text("t".into()).type_code(), Some(5));
assert_eq!(Value::Timestamp(0).type_code(), Some(8));
assert_eq!(Value::Date(20260710).type_code(), Some(9));
assert_eq!(Value::Blob(vec![0]).type_code(), Some(10));
assert_eq!(Value::String("s".into()).type_code(), Some(11));
assert_eq!(Value::Object(vec![]).type_code(), Some(12));
assert_eq!(Value::Null.type_code(), None);
}
#[test]
fn data_type_of_null_is_none() {
assert_eq!(Value::Null.data_type(), None);
assert!(Value::Null.is_null());
assert!(!Value::Int32(7).is_null());
}
fn object_size_bytes(size: u64) -> Vec<u8> {
let mut value = size.to_be_bytes().to_vec();
value.extend_from_slice(b"internal/path/1.bin");
value
}
#[test]
fn object_bytes_to_string_formats_all_units() {
assert_eq!(
object_bytes_to_string(&object_size_bytes(1023)).unwrap(),
"(Object) 1023 B"
);
assert_eq!(
object_bytes_to_string(&object_size_bytes(1024)).unwrap(),
"(Object) 1.00 KB"
);
assert_eq!(
object_bytes_to_string(&object_size_bytes(1024 * 1024)).unwrap(),
"(Object) 1.00 MB"
);
assert_eq!(
object_bytes_to_string(&object_size_bytes(1024 * 1024 * 1024)).unwrap(),
"(Object) 1.00 GB"
);
}
#[test]
fn object_bytes_to_string_rejects_short_input() {
assert!(matches!(
object_bytes_to_string(&[0; 7]),
Err(Error::Decode(m)) if m.contains("at least 8 bytes")
));
}
}