1use crate::codec::column_codec::{
26 encode_attr_col, encode_row_kinds, encode_surrogates, encode_timestamps_col,
27};
28use crate::codec::coord_rle::encode_coord_axis_rle;
29use crate::codec::tag::CodecTag;
30use crate::error::{ArrayError, ArrayResult};
31use crate::tile::sparse_tile::SparseTile;
32
33const PAYLOAD_VERSION: u8 = 1;
34
35const STRUCTURAL_MIN_CELLS: usize = 8;
37
38fn choose_tag(tile: &SparseTile) -> CodecTag {
39 let cell_count = tile.surrogates.len();
40 if cell_count < STRUCTURAL_MIN_CELLS {
41 return CodecTag::Raw;
42 }
43 let all_sentinel = !tile.row_kinds.is_empty()
45 && tile.row_kinds.iter().all(|&k| k != 0)
46 && tile.attr_cols.iter().all(|col| col.is_empty());
47 if all_sentinel {
48 return CodecTag::Raw;
49 }
50 CodecTag::Structural
51}
52
53fn write_framed(chunk: &[u8], out: &mut Vec<u8>) {
54 out.extend_from_slice(&(chunk.len() as u32).to_le_bytes());
55 out.extend_from_slice(chunk);
56}
57
58pub fn encode_sparse_tile(tile: &SparseTile, out: &mut Vec<u8>) -> ArrayResult<()> {
61 tile.check_stored_identities()?;
64 let tag = choose_tag(tile);
65 out.push(tag.as_byte());
66 out.push(PAYLOAD_VERSION);
67
68 match tag {
69 CodecTag::Raw => encode_raw(tile, out),
70 CodecTag::Structural => encode_structural(tile, out),
71 }
72}
73
74fn encode_raw(tile: &SparseTile, out: &mut Vec<u8>) -> ArrayResult<()> {
75 let bytes = zerompk::to_msgpack_vec(tile).map_err(|e| ArrayError::SegmentCorruption {
76 detail: format!("raw tile encode: {e}"),
77 })?;
78 out.extend_from_slice(&bytes);
79 Ok(())
80}
81
82fn encode_structural(tile: &SparseTile, out: &mut Vec<u8>) -> ArrayResult<()> {
83 let cell_count = tile.surrogates.len() as u32;
84 let axis_count = tile.dim_dicts.len() as u32;
85 out.extend_from_slice(&cell_count.to_le_bytes());
86 out.extend_from_slice(&axis_count.to_le_bytes());
87
88 for dict in &tile.dim_dicts {
90 let mut axis_buf = Vec::new();
91 encode_coord_axis_rle(dict, &mut axis_buf)?;
92 write_framed(&axis_buf, out);
93 }
94
95 let live_surrogates: Vec<_> = tile.surrogates.iter().copied().flatten().collect();
98 let surr_bytes = encode_surrogates(&live_surrogates)?;
99 write_framed(&surr_bytes, out);
100
101 let rk_bytes = encode_row_kinds(&tile.row_kinds);
103 write_framed(&rk_bytes, out);
104
105 let sys_bytes = encode_timestamps_col(&[]);
107 write_framed(&sys_bytes, out);
110
111 let vf_bytes = encode_timestamps_col(&tile.valid_from_ms);
112 write_framed(&vf_bytes, out);
113
114 let vu_bytes = encode_timestamps_col(&tile.valid_until_ms);
115 write_framed(&vu_bytes, out);
116
117 let attr_count = tile.attr_cols.len() as u32;
119 out.extend_from_slice(&attr_count.to_le_bytes());
120 for col in &tile.attr_cols {
121 let col_bytes = encode_attr_col(col)?;
122 write_framed(&col_bytes, out);
123 }
124
125 Ok(())
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use crate::codec::tile_decode::decode_sparse_tile;
132 use crate::schema::ArraySchemaBuilder;
133 use crate::schema::attr_spec::{AttrSpec, AttrType};
134 use crate::schema::dim_spec::{DimSpec, DimType};
135 use crate::tile::sparse_tile::{RowKind, SparseRow, SparseTileBuilder};
136 use crate::types::cell_value::value::CellValue;
137 use crate::types::coord::value::CoordValue;
138 use crate::types::domain::{Domain, DomainBound};
139 use nodedb_types::{OPEN_UPPER, Surrogate};
140
141 fn schema() -> crate::schema::ArraySchema {
142 ArraySchemaBuilder::new("t")
143 .dim(DimSpec::new(
144 "x",
145 DimType::Int64,
146 Domain::new(DomainBound::Int64(0), DomainBound::Int64(1_000_000)),
147 ))
148 .dim(DimSpec::new(
149 "y",
150 DimType::Int64,
151 Domain::new(DomainBound::Int64(0), DomainBound::Int64(1_000_000)),
152 ))
153 .attr(AttrSpec::new("v", AttrType::Int64, true))
154 .tile_extents(vec![1000, 1000])
155 .build()
156 .unwrap()
157 }
158
159 fn make_tile(s: &crate::schema::ArraySchema, n: usize) -> SparseTile {
160 let mut b = SparseTileBuilder::new(s);
161 for i in 0..n {
162 b.push_row(SparseRow {
163 coord: &[CoordValue::Int64(i as i64), CoordValue::Int64(i as i64 * 2)],
164 attrs: &[CellValue::Int64(i as i64)],
165 surrogate: Some(Surrogate::new(i as u32 + 1)),
166 valid_from_ms: i as i64 * 10,
167 valid_until_ms: OPEN_UPPER,
168 kind: RowKind::Live,
169 })
170 .unwrap();
171 }
172 b.build()
173 }
174
175 #[test]
176 fn small_tile_uses_raw_tag() {
177 let s = schema();
178 let tile = make_tile(&s, 3);
179 let mut buf = Vec::new();
180 encode_sparse_tile(&tile, &mut buf).unwrap();
181 assert_eq!(buf[0], CodecTag::Raw.as_byte());
182 }
183
184 #[test]
185 fn large_tile_uses_structural_tag() {
186 let s = schema();
187 let tile = make_tile(&s, 20);
188 let mut buf = Vec::new();
189 encode_sparse_tile(&tile, &mut buf).unwrap();
190 assert_eq!(buf[0], CodecTag::Structural.as_byte());
191 }
192
193 #[test]
194 fn small_tile_roundtrip() {
195 let s = schema();
196 let tile = make_tile(&s, 5);
197 let mut buf = Vec::new();
198 encode_sparse_tile(&tile, &mut buf).unwrap();
199 let decoded = decode_sparse_tile(&buf).unwrap();
200 assert_eq!(decoded.surrogates, tile.surrogates);
201 assert_eq!(decoded.valid_from_ms, tile.valid_from_ms);
202 assert_eq!(decoded.row_kinds, tile.row_kinds);
203 }
204
205 #[test]
206 fn large_tile_roundtrip() {
207 let s = schema();
208 let tile = make_tile(&s, 100);
209 let mut buf = Vec::new();
210 encode_sparse_tile(&tile, &mut buf).unwrap();
211 let decoded = decode_sparse_tile(&buf).unwrap();
212 assert_eq!(decoded.surrogates, tile.surrogates);
213 assert_eq!(decoded.attr_cols, tile.attr_cols);
214 assert_eq!(decoded.dim_dicts.len(), tile.dim_dicts.len());
215 }
216
217 #[test]
218 fn sentinel_only_tile_uses_raw() {
219 let s = schema();
220 let mut b = SparseTileBuilder::new(&s);
221 for i in 0..20 {
222 b.push_row(SparseRow {
223 coord: &[CoordValue::Int64(i), CoordValue::Int64(i)],
224 attrs: &[],
225 surrogate: None,
226 valid_from_ms: 0,
227 valid_until_ms: OPEN_UPPER,
228 kind: RowKind::Tombstone,
229 })
230 .unwrap();
231 }
232 let tile = b.build();
233 let mut buf = Vec::new();
234 encode_sparse_tile(&tile, &mut buf).unwrap();
235 assert_eq!(buf[0], CodecTag::Raw.as_byte());
236 }
237
238 #[test]
239 fn version_byte_is_one() {
240 let s = schema();
241 let tile = make_tile(&s, 20);
242 let mut buf = Vec::new();
243 encode_sparse_tile(&tile, &mut buf).unwrap();
244 assert_eq!(buf[1], PAYLOAD_VERSION);
245 }
246
247 #[test]
248 fn empty_tile_encodes_and_decodes() {
249 let s = schema();
250 let tile = SparseTile::empty(&s);
251 let mut buf = Vec::new();
252 encode_sparse_tile(&tile, &mut buf).unwrap();
253 let decoded = decode_sparse_tile(&buf).unwrap();
254 assert_eq!(decoded.surrogates, tile.surrogates);
255 }
256
257 #[test]
260 fn structural_roundtrip_keeps_sentinel_rows_without_identity() {
261 let s = schema();
262 let mut b = SparseTileBuilder::new(&s);
263 for i in 0..20i64 {
264 let coord = [CoordValue::Int64(i), CoordValue::Int64(i)];
265 if i % 3 == 0 {
266 b.push_row(SparseRow::sentinel(&coord, RowKind::Tombstone))
267 .unwrap();
268 } else {
269 b.push_row(SparseRow::live(
270 &coord,
271 &[CellValue::Int64(i)],
272 Surrogate::new(100 + i as u32),
273 0,
274 OPEN_UPPER,
275 ))
276 .unwrap();
277 }
278 }
279 let tile = b.build();
280 let mut buf = Vec::new();
281 encode_sparse_tile(&tile, &mut buf).unwrap();
282 assert_eq!(buf[0], CodecTag::Structural.as_byte());
283 let decoded = decode_sparse_tile(&buf).unwrap();
284 assert_eq!(decoded.surrogates, tile.surrogates);
285 assert_eq!(decoded.surrogates[0], None);
286 assert_eq!(decoded.surrogates[1], Some(Surrogate::new(101)));
287 }
288
289 #[test]
291 fn a_derived_tile_is_refused_by_the_encoder() {
292 let s = schema();
293 let mut b = SparseTileBuilder::new(&s);
294 b.push(
295 &[CoordValue::Int64(1), CoordValue::Int64(1)],
296 &[CellValue::Int64(1)],
297 )
298 .unwrap();
299 let mut buf = Vec::new();
300 assert!(encode_sparse_tile(&b.build(), &mut buf).is_err());
301 assert!(buf.is_empty(), "nothing is written for a refused tile");
302 }
303}