1use nodedb_types::columnar::ColumnType;
11use nodedb_types::value::Value;
12use nodedb_types::value_from_msgpack;
13
14use crate::error::ColumnarError;
15
16use super::types::DecodedColumn;
17
18const ID_WIDTH: usize = 16;
20
21const F32_WIDTH: usize = 4;
23
24pub fn decoded_cell_value(
30 col: &DecodedColumn,
31 row: usize,
32 declared: &ColumnType,
33) -> Result<Value, ColumnarError> {
34 let is_valid = |valid: &[bool]| valid.get(row).copied().unwrap_or(false);
35 match col {
36 DecodedColumn::Int64 { values, valid } | DecodedColumn::Timestamp { values, valid } => {
38 Ok(match values.get(row) {
39 Some(&v) if is_valid(valid) => declared.time_cell(v),
40 _ => Value::Null,
41 })
42 }
43 DecodedColumn::Float64 { values, valid } => Ok(match values.get(row) {
44 Some(&v) if is_valid(valid) => Value::Float(v),
45 _ => Value::Null,
46 }),
47 DecodedColumn::Bool { values, valid } => Ok(match values.get(row) {
48 Some(&v) if is_valid(valid) => Value::Bool(v),
49 _ => Value::Null,
50 }),
51 DecodedColumn::DictEncoded {
52 ids,
53 dictionary,
54 valid,
55 } => {
56 let Some(&id) = ids.get(row).filter(|_| is_valid(valid)) else {
57 return Ok(Value::Null);
58 };
59 let text = usize::try_from(id)
60 .ok()
61 .and_then(|id| dictionary.get(id))
62 .ok_or_else(|| {
63 corruption(format!(
64 "dictionary ID {id} is outside a dictionary of {} entries",
65 dictionary.len()
66 ))
67 })?;
68 Ok(Value::String(text.clone()))
69 }
70 DecodedColumn::Binary {
71 data,
72 offsets,
73 valid,
74 } => {
75 if !is_valid(valid) {
76 return Ok(Value::Null);
77 }
78 let bytes = cell_bytes(data, offsets, row)?;
79 binary_cell_value(bytes, declared)
80 }
81 }
82}
83
84fn cell_bytes<'a>(data: &'a [u8], offsets: &[u32], row: usize) -> Result<&'a [u8], ColumnarError> {
86 let bound = |i: usize| offsets.get(i).and_then(|&o| usize::try_from(o).ok());
87 bound(row)
88 .zip(row.checked_add(1).and_then(bound))
89 .and_then(|(start, end)| data.get(start..end))
90 .ok_or_else(|| {
91 corruption(format!(
92 "row {row} has no byte range in a binary column of {} offsets and {} bytes",
93 offsets.len(),
94 data.len()
95 ))
96 })
97}
98
99fn binary_cell_value(bytes: &[u8], declared: &ColumnType) -> Result<Value, ColumnarError> {
101 Ok(match declared {
102 ColumnType::String
103 | ColumnType::Regex
104 | ColumnType::SparseVector
105 | ColumnType::Geometry => Value::String(utf8(bytes, declared)?.to_owned()),
106 ColumnType::Bytes
107 | ColumnType::Array
108 | ColumnType::Set
109 | ColumnType::Range
110 | ColumnType::Record => Value::Bytes(bytes.to_vec()),
111 ColumnType::Json if bytes.is_empty() => Value::Null,
112 ColumnType::Json => value_from_msgpack(bytes)
113 .map_err(|e| corruption(format!("JSON cell is not MessagePack: {e}")))?,
114 ColumnType::Decimal { .. } => Value::Decimal(rust_decimal::Decimal::deserialize(id_cell(
115 bytes, declared,
116 )?)),
117 ColumnType::Uuid => {
118 Value::Uuid(uuid::Uuid::from_bytes(id_cell(bytes, declared)?).to_string())
119 }
120 ColumnType::Ulid => {
121 Value::Ulid(ulid::Ulid::from_bytes(id_cell(bytes, declared)?).to_string())
122 }
123 ColumnType::Vector(dim) => vector_cell(bytes, *dim)?,
124 ColumnType::Int64
125 | ColumnType::Float64
126 | ColumnType::Bool
127 | ColumnType::Timestamp
128 | ColumnType::Timestamptz
129 | ColumnType::SystemTimestamp
130 | ColumnType::Duration => {
131 return Err(corruption(format!(
132 "a {declared} column has no binary cells"
133 )));
134 }
135 _ => Value::Bytes(bytes.to_vec()),
138 })
139}
140
141fn utf8<'a>(bytes: &'a [u8], declared: &ColumnType) -> Result<&'a str, ColumnarError> {
142 std::str::from_utf8(bytes).map_err(|e| corruption(format!("{declared} cell is not UTF-8: {e}")))
143}
144
145fn id_cell(bytes: &[u8], declared: &ColumnType) -> Result<[u8; ID_WIDTH], ColumnarError> {
146 <[u8; ID_WIDTH]>::try_from(bytes).map_err(|_| {
147 corruption(format!(
148 "{declared} cell holds {} bytes, not {ID_WIDTH}",
149 bytes.len()
150 ))
151 })
152}
153
154fn vector_cell(bytes: &[u8], dim: u32) -> Result<Value, ColumnarError> {
156 let expected = usize::try_from(dim)
157 .ok()
158 .and_then(|d| d.checked_mul(F32_WIDTH));
159 if expected != Some(bytes.len()) {
160 return Err(corruption(format!(
161 "VECTOR({dim}) cell holds {} bytes",
162 bytes.len()
163 )));
164 }
165 let (chunks, _) = bytes.as_chunks::<F32_WIDTH>();
166 Ok(Value::Array(
167 chunks
168 .iter()
169 .map(|c| Value::Float(f64::from(f32::from_le_bytes(*c))))
170 .collect(),
171 ))
172}
173
174fn corruption(reason: String) -> ColumnarError {
175 ColumnarError::Corruption {
176 segment_id: None,
177 reason,
178 offset: None,
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use std::collections::HashMap;
185
186 use nodedb_types::columnar::{ColumnDef, ColumnType, ColumnarSchema};
187 use nodedb_types::value::Value;
188 use nodedb_types::{NdbDateTime, NdbDuration};
189
190 use super::decoded_cell_value;
191 use crate::memtable::ColumnarMemtable;
192 use crate::reader::SegmentReader;
193 use crate::test_support::test_memory;
194 use crate::writer::{PROFILE_PLAIN, SegmentWriter};
195
196 const ROWS: usize = 1500;
198
199 const BASE_MICROS: i64 = 1_700_000_000_000_000;
200
201 fn every_type_schema() -> ColumnarSchema {
202 ColumnarSchema::new(vec![
203 ColumnDef::required("id", ColumnType::Int64).with_primary_key(),
204 ColumnDef::nullable("f", ColumnType::Float64),
205 ColumnDef::nullable("b", ColumnType::Bool),
206 ColumnDef::nullable("s", ColumnType::String),
207 ColumnDef::nullable("tag", ColumnType::String),
208 ColumnDef::nullable("raw", ColumnType::Bytes),
209 ColumnDef::nullable("ts", ColumnType::Timestamp),
210 ColumnDef::nullable("tz", ColumnType::Timestamptz),
211 ColumnDef::nullable("sys", ColumnType::SystemTimestamp),
212 ColumnDef::nullable("dec", ColumnType::Decimal(None)),
213 ColumnDef::nullable("u", ColumnType::Uuid),
214 ColumnDef::nullable("ul", ColumnType::Ulid),
215 ColumnDef::nullable("geo", ColumnType::Geometry),
216 ColumnDef::nullable("vec", ColumnType::Vector(3)),
217 ColumnDef::nullable("j", ColumnType::Json),
218 ColumnDef::nullable("dur", ColumnType::Duration),
219 ColumnDef::nullable("re", ColumnType::Regex),
220 ColumnDef::nullable("arr", ColumnType::Array),
221 ColumnDef::nullable("sparse", ColumnType::SparseVector),
222 ])
223 .expect("valid schema")
224 }
225
226 fn ulid_text(i: usize) -> String {
227 let mut bytes = [0u8; 16];
228 bytes[..8].copy_from_slice(&(i as u64 + 1).to_be_bytes());
229 bytes[15] = 7;
230 ulid::Ulid::from_bytes(bytes).to_string()
231 }
232
233 fn json_cell(i: usize) -> Value {
234 if i.is_multiple_of(5) {
235 Value::String("\"scalar\"".into())
237 } else {
238 Value::Object(HashMap::from([
239 ("n".to_string(), Value::Integer(i as i64)),
240 ("tag".to_string(), Value::String("x".into())),
241 ]))
242 }
243 }
244
245 fn row(i: usize) -> Vec<Value> {
246 let mut values = vec![Value::Integer(i as i64)];
247 if i % 11 == 4 {
248 values.extend(std::iter::repeat_n(Value::Null, 18));
249 return values;
250 }
251 values.extend([
252 Value::Float(i as f64 * 0.5),
253 Value::Bool(i.is_multiple_of(2)),
254 Value::String(format!("row-{i}")),
255 Value::String(["alpha", "beta", "gamma"][i % 3].into()),
256 Value::Bytes(vec![i as u8, 0xFF, 0x00]),
257 Value::NaiveDateTime(NdbDateTime::from_micros(BASE_MICROS + i as i64)),
258 Value::DateTime(NdbDateTime::from_micros(BASE_MICROS - i as i64)),
259 Value::Integer(i as i64 * 1_000),
260 Value::Decimal(rust_decimal::Decimal::new(i as i64 * 25, 2)),
261 Value::Uuid(uuid::Uuid::from_u128(i as u128 + 1).to_string()),
262 Value::Ulid(ulid_text(i)),
263 Value::String(format!("POINT({i} 1)")),
264 Value::Array(vec![
265 Value::Float(i as f64),
266 Value::Float(0.5),
267 Value::Float(-1.25),
268 ]),
269 json_cell(i),
270 Value::Duration(NdbDuration::from_micros(i as i64 * 3)),
271 Value::Regex(format!("^r{i}$")),
272 Value::Array(vec![Value::Integer(i as i64), Value::String("a".into())]),
273 Value::String(format!("{{{i}: 0.5}}")),
274 ]);
275 values
276 }
277
278 #[test]
282 fn a_flushed_cell_reads_as_its_memtable_value() {
283 let schema = every_type_schema();
284 let mut mt = ColumnarMemtable::new(&schema);
285 for i in 0..ROWS {
286 mt.append_row(&row(i)).expect("append");
287 }
288 let expected: Vec<Vec<Value>> = (0..ROWS)
289 .map(|i| mt.get_row(i).expect("read").expect("memtable row"))
290 .collect();
291
292 let (schema, columns, row_count) = mt.drain_optimized();
293 let segment = SegmentWriter::new(PROFILE_PLAIN, test_memory())
294 .write_segment(&schema, &columns, row_count, None)
295 .expect("write segment");
296 let reader = SegmentReader::open(&segment).expect("open segment");
297 let indices: Vec<usize> = (0..schema.columns.len()).collect();
298 let decoded = reader.read_columns(&indices, &[]).expect("read columns");
299
300 for (i, memtable_row) in expected.iter().enumerate() {
301 for ((col, def), want) in decoded.iter().zip(&schema.columns).zip(memtable_row) {
302 let got = decoded_cell_value(col, i, &def.column_type).expect("decodable cell");
303 assert_eq!(&got, want, "row {i} column '{}'", def.name);
304 }
305 }
306
307 let first = &expected[1];
309 assert_eq!(first[10], Value::Uuid(uuid::Uuid::from_u128(2).to_string()));
310 assert_eq!(first[11], Value::Ulid(ulid_text(1)));
311 assert_eq!(first[12], Value::String("POINT(1 1)".into()));
312 assert_eq!(first[15], Value::Integer(3));
313 assert_eq!(first[16], Value::String("^r1$".into()));
314 assert_eq!(expected[4][10], Value::Null);
315 }
316
317 #[test]
320 fn a_corrupt_binary_cell_is_an_error() {
321 let col = crate::reader::DecodedColumn::Binary {
322 data: vec![1, 2, 3],
323 offsets: vec![0, 3],
324 valid: vec![true],
325 };
326 for ty in [
327 ColumnType::Uuid,
328 ColumnType::Ulid,
329 ColumnType::Decimal(None),
330 ColumnType::Vector(2),
331 ] {
332 assert!(
333 decoded_cell_value(&col, 0, &ty).is_err(),
334 "{ty} must reject a 3-byte cell"
335 );
336 }
337 let not_msgpack = crate::reader::DecodedColumn::Binary {
339 data: vec![0xC1],
340 offsets: vec![0, 1],
341 valid: vec![true],
342 };
343 assert!(decoded_cell_value(¬_msgpack, 0, &ColumnType::Json).is_err());
344 let not_utf8 = crate::reader::DecodedColumn::Binary {
345 data: vec![0xFF],
346 offsets: vec![0, 1],
347 valid: vec![true],
348 };
349 assert!(decoded_cell_value(¬_utf8, 0, &ColumnType::String).is_err());
350 assert_eq!(
351 decoded_cell_value(¬_utf8, 0, &ColumnType::Bytes).expect("bytes"),
352 Value::Bytes(vec![0xFF])
353 );
354 }
355}