| 94 | const ATTR_TAG_MSGPACK: u8 = 2; // String, Bytes, Null — zerompk per-value |
| 95 | |
| 96 | pub fn encode_attr_col(values: &[CellValue]) -> ArrayResult<Vec<u8>> { |
| 97 | if values.is_empty() { |
| 98 | let mut out = vec![ATTR_TAG_MSGPACK]; |
| 99 | out.extend_from_slice(&0u32.to_le_bytes()); |
| 100 | return Ok(out); |
| 101 | } |
| 102 | |
| 103 | // Check if all values are Int64 or Float64 — only then use numeric codec. |
| 104 | let all_int = values |
| 105 | .iter() |
| 106 | .all(|v| matches!(v, CellValue::Int64(_) | CellValue::Null)); |
| 107 | let all_float = values |
| 108 | .iter() |
| 109 | .all(|v| matches!(v, CellValue::Float64(_) | CellValue::Null)); |
| 110 | |
| 111 | if all_int { |
| 112 | let ints: Vec<i64> = values |
| 113 | .iter() |
| 114 | .map(|v| match v { |
| 115 | CellValue::Int64(i) => *i, |
| 116 | _ => 0, |
| 117 | }) |
| 118 | .collect(); |
| 119 | let encoded = nodedb_codec::fastlanes::encode(&ints); |
| 120 | let mut out = vec![ATTR_TAG_INT64]; |
| 121 | out.extend_from_slice(&encoded); |
| 122 | return Ok(out); |
| 123 | } |
| 124 | |
| 125 | if all_float { |
| 126 | // Gorilla XOR-encodes f64 series — exploits common-prefix bits across |
| 127 | // adjacent values, which dominates the size of monotonic / smooth |
| 128 | // numeric columns. fastlanes-on-bits used to live here but treats |
| 129 | // each f64 as an independent i64, missing the inter-value redundancy. |
| 130 | let floats: Vec<f64> = values |
| 131 | .iter() |
| 132 | .map(|v| match v { |
| 133 | CellValue::Float64(f) => *f, |
| 134 | _ => 0.0, |
| 135 | }) |
| 136 | .collect(); |
| 137 | let encoded = nodedb_codec::gorilla::encode_f64(&floats); |
| 138 | let mut out = vec![ATTR_TAG_FLOAT64]; |
| 139 | out.extend_from_slice(&encoded); |
| 140 | return Ok(out); |
| 141 | } |
| 142 | |
| 143 | // Generic zerompk fallback for String / Bytes / mixed / Null. |
| 144 | let mut out = vec![ATTR_TAG_MSGPACK]; |
| 145 | out.extend_from_slice(&(values.len() as u32).to_le_bytes()); |
| 146 | for v in values { |
| 147 | let bytes = zerompk::to_msgpack_vec(v).map_err(|e| ArrayError::SegmentCorruption { |
| 148 | detail: format!("attr col encode: {e}"), |
| 149 | })?; |
| 150 | out.extend_from_slice(&(bytes.len() as u32).to_le_bytes()); |
| 151 | out.extend_from_slice(&bytes); |
| 152 | } |
| 153 | Ok(out) |