1use std::num::NonZeroUsize;
18use std::time::{Duration, UNIX_EPOCH};
19use arrow_array::{
21 Array, BinaryArray, BooleanArray, Float32Array, Float64Array, Int32Array, Int64Array, ListArray, StringArray, StructArray,
22 TimestampNanosecondArray, UInt32Array, UInt64Array,
23};
24use super::DecodedBatch;
26use crate::schema::{
27 Cardinality,
28 graph::{Field, Kind, Schema},
29};
30use crate::storage::{ObjectKey, ObjectValue, PayloadEncoding, RecordInput, StreamInput};
31use crate::{Error, Identifier, Result};
32
33const MAX_DEPTH: usize = 64;
36
37#[tracing::instrument(name = "storage.arrow.encode_payloads_memory", level = "trace", skip_all, fields(rows = payload.len(), max_bytes = max_bytes.get(), outcome = "cancelled"))]
58pub fn encode_payloads_memory(schema: &Schema, payload: &StructArray, encoding: PayloadEncoding, max_bytes: NonZeroUsize) -> Result<usize> {
59 let result: Result<usize> = (|| {
60 let mut count = payload_counter(payload.len(), max_bytes)?;
61 for row in 0..payload.len() {
62 encode_message(schema, schema.root, payload, row, encoding, 0, &mut count)?;
63 }
64 Ok(count.bytes)
65 })();
66 tracing::Span::current().record("outcome", if result.is_ok() { "completed" } else { "failed" });
67 result
68}
69
70#[tracing::instrument(name = "storage.arrow.encode_payloads", level = "trace", skip_all, fields(rows = payload.len(), max_bytes = max_bytes.get(), outcome = "cancelled"))]
71pub fn encode_payloads(schema: &Schema, payload: &StructArray, encoding: PayloadEncoding, max_bytes: NonZeroUsize) -> Result<Vec<Vec<u8>>> {
72 let result: Result<Vec<Vec<u8>>> = (|| {
73 let mut count = payload_counter(payload.len(), max_bytes)?;
74 let mut rows = Vec::with_capacity(payload.len());
75 for row in 0..payload.len() {
76 let previous = count.bytes;
77 encode_message(schema, schema.root, payload, row, encoding, 0, &mut count)?;
78 let mut bytes = Vec::with_capacity(count.bytes - previous);
79 encode_message(schema, schema.root, payload, row, encoding, 0, &mut bytes)?;
80 rows.push(bytes);
81 }
82 Ok(rows)
83 })();
84 tracing::Span::current().record("outcome", if result.is_ok() { "completed" } else { "failed" });
85 result
86}
87
88pub fn records(scope: Option<&Identifier>, batch: &DecodedBatch, schema: &Schema) -> Result<Vec<RecordInput>> {
89 let data = batch.data.as_record_batch();
90 let metadata = struct_column(data.column_by_name("_storage").ok_or_else(invalid)?.as_ref())?;
91 let payload = struct_column(data.column_by_name("payload").ok_or_else(invalid)?.as_ref())?;
92 (0..data.num_rows())
93 .map(|row| {
94 if metadata.is_null(row) {
95 return Err(invalid());
96 }
97 let authored_scope = optional_id(metadata, "scope", row)?;
98 if scope.is_some_and(|expected| authored_scope.as_ref().is_some_and(|actual| actual != expected)) {
99 return Err(invalid());
100 }
101 let source_time = metadata
102 .column_by_name("source_time")
103 .filter(|column| !column.is_null(row))
104 .map(|column| {
105 let nanos = typed::<TimestampNanosecondArray>(column.as_ref())?.value(row);
106 if nanos >= 0 {
107 UNIX_EPOCH.checked_add(Duration::from_nanos(nanos as u64))
108 } else {
109 UNIX_EPOCH.checked_sub(Duration::from_nanos(nanos.unsigned_abs()))
110 }
111 .ok_or_else(invalid)
112 })
113 .transpose()?;
114 let mut bytes = Vec::new();
115 encode_message(schema, schema.root, payload, row, batch.schema.payload_encoding, 0, &mut bytes)?;
116 Ok(RecordInput {
117 schema: batch.schema.clone(),
118 scope: scope.cloned().or(authored_scope),
119 item: required_id(metadata, "item", row)?,
120 source: optional_id(metadata, "source", row)?,
121 source_time,
122 payload: bytes,
123 })
124 })
125 .collect()
126}
127
128pub fn streams(scope: Option<&Identifier>, batch: &DecodedBatch, schema: &Schema) -> Result<Vec<StreamInput>> {
129 let records = records(scope, batch, schema)?;
130 let metadata = struct_column(batch.data.as_record_batch().column_by_name("_storage").ok_or_else(invalid)?.as_ref())?;
131 let sequence = typed::<UInt64Array>(metadata.column_by_name("sequence").ok_or_else(invalid)?.as_ref())?;
132 records
133 .into_iter()
134 .enumerate()
135 .map(|(row, record)| {
136 if sequence.is_null(row) {
137 return Err(invalid());
138 }
139 if optional_id(metadata, "stream", row)?.is_some_and(|stream| stream != batch.destination) {
140 return Err(invalid());
141 }
142 Ok(StreamInput { record, stream: batch.destination.clone(), sequence: sequence.value(row) })
143 })
144 .collect()
145}
146
147pub fn objects(batch: &DecodedBatch, schema: &Schema, max_key_bytes: NonZeroUsize) -> Result<Vec<ObjectValue>> {
148 let data = batch.data.as_record_batch();
149 let metadata = struct_column(data.column_by_name("_storage").ok_or_else(invalid)?.as_ref())?;
150 let items = typed::<StringArray>(metadata.column_by_name("item").ok_or_else(invalid)?.as_ref())?;
151 let payload = struct_column(data.column_by_name("payload").ok_or_else(invalid)?.as_ref())?;
152 (0..data.num_rows())
153 .map(|row| {
154 if metadata.is_null(row) || items.is_null(row) {
155 return Err(invalid());
156 }
157 let mut bytes = Vec::new();
158 encode_message(schema, schema.root, payload, row, batch.schema.payload_encoding, 0, &mut bytes)?;
159 Ok(ObjectValue { schema: batch.schema.clone(), item: ObjectKey::new(items.value(row), max_key_bytes)?, payload: bytes })
160 })
161 .collect()
162}
163
164fn payload_counter(rows: usize, max_bytes: NonZeroUsize) -> Result<Counter> {
167 let metadata = rows
168 .checked_mul(std::mem::size_of::<Vec<u8>>())
169 .and_then(|bytes| bytes.checked_add(std::mem::size_of::<Vec<Vec<u8>>>()))
170 .filter(|bytes| *bytes <= max_bytes.get())
171 .ok_or_else(exhausted)?;
172 Ok(Counter { bytes: metadata, limit: max_bytes.get() })
173}
174
175trait Sink {
176 fn append(&mut self, value: &[u8]) -> Result<()>;
177}
178struct Counter {
179 bytes: usize,
180 limit: usize,
181}
182impl Sink for Counter {
183 fn append(&mut self, value: &[u8]) -> Result<()> {
184 self.bytes = self.bytes.checked_add(value.len()).filter(|bytes| *bytes <= self.limit).ok_or_else(exhausted)?;
185 Ok(())
186 }
187}
188impl Sink for Vec<u8> {
189 fn append(&mut self, value: &[u8]) -> Result<()> {
190 self.extend_from_slice(value);
191 Ok(())
192 }
193}
194fn scalar<T: serde::Serialize>(value: T, output: &mut impl Sink) -> Result<()> {
195 let mut bytes = [0u8; 16];
196 let encoded = postcard::to_slice(&value, &mut bytes).map_err(|_| invalid())?;
197 output.append(encoded)
198}
199fn encode_message(
200 schema: &Schema, definition: usize, values: &StructArray, row: usize, encoding: PayloadEncoding, depth: usize, output: &mut impl Sink,
201) -> Result<()> {
202 if depth > MAX_DEPTH || values.is_null(row) {
203 return Err(invalid());
204 }
205 let fields = schema.definitions.get(definition).ok_or_else(invalid)?;
206 if fields.iter().any(|field| field.cardinality == Cardinality::Variant) {
207 let mut selected = None;
208 for (index, field) in fields.iter().enumerate() {
209 if let Some(value) = values.column_by_name(&field.name)
210 && !value.is_null(row)
211 {
212 if selected.is_some() {
213 return Err(invalid());
214 }
215 selected = Some((index, field, value));
216 }
217 }
218 let (index, field, value) = selected.ok_or_else(invalid)?;
219 let discriminant = typed::<UInt32Array>(values.column_by_name("__variant").ok_or_else(invalid)?.as_ref())?;
220 if discriminant.is_null(row) || discriminant.value(row) as usize != index {
221 return Err(invalid());
222 }
223 if encoding == PayloadEncoding::PostcardV1 {
224 scalar(index as u32, output)?;
225 }
226 return encode_value(schema, field, value.as_ref(), row, encoding, depth + 1, output);
227 }
228 for field in fields {
229 let value = values.column_by_name(&field.name).ok_or_else(invalid)?;
230 match field.cardinality {
231 Cardinality::Optional => {
232 if encoding == PayloadEncoding::PostcardV1 {
233 scalar(!value.is_null(row), output)?;
234 }
235 if value.is_null(row) {
236 continue;
237 }
238 }
239 Cardinality::Repeated => {
240 let lists = typed::<ListArray>(value.as_ref())?;
241 if lists.is_null(row) {
242 return Err(invalid());
243 }
244 let start = lists.value_offsets()[row] as usize;
245 let end = lists.value_offsets()[row + 1] as usize;
246 if encoding == PayloadEncoding::PostcardV1 {
247 scalar((end - start) as u64, output)?;
248 }
249 for index in start..end {
250 encode_value(schema, field, lists.values().as_ref(), index, encoding, depth + 1, output)?;
251 }
252 continue;
253 }
254 Cardinality::Required | Cardinality::Variant => {}
255 }
256 encode_value(schema, field, value.as_ref(), row, encoding, depth + 1, output)?;
257 }
258 Ok(())
259}
260fn encode_value(
261 schema: &Schema, field: &Field, array: &dyn Array, row: usize, encoding: PayloadEncoding, depth: usize, output: &mut impl Sink,
262) -> Result<()> {
263 if array.is_null(row) {
264 return Err(invalid());
265 }
266 let protobuf = encoding == PayloadEncoding::ProtobufV1;
267 macro_rules! number {
268 ($array:ty, $wire:expr, $encode:expr) => {{
269 let value = typed::<$array>(array)?.value(row);
270 if protobuf {
271 varint((u64::from(field.tag) << 3) | $wire, output)?;
272 ($encode)(value, output)?;
273 } else {
274 scalar(value, output)?;
275 }
276 }};
277 }
278 match field.kind {
279 Kind::Bool => number!(BooleanArray, 0, |value: bool, out: &mut _| varint(u64::from(value), out)),
280 Kind::U32 => number!(UInt32Array, 0, |value: u32, out: &mut _| varint(u64::from(value), out)),
281 Kind::U64 => number!(UInt64Array, 0, |value: u64, out: &mut _| varint(value, out)),
282 Kind::I32 => number!(Int32Array, 0, |value: i32, out: &mut _| varint(value as i64 as u64, out)),
283 Kind::I64 => number!(Int64Array, 0, |value: i64, out: &mut _| varint(value as u64, out)),
284 Kind::F32 => number!(Float32Array, 5, |value: f32, out: &mut _| Sink::append(out, &value.to_le_bytes())),
285 Kind::F64 => number!(Float64Array, 1, |value: f64, out: &mut _| Sink::append(out, &value.to_le_bytes())),
286 Kind::String => {
287 let value = typed::<StringArray>(array)?.value(row);
288 if protobuf {
289 header(field.tag, value.len(), output)?;
290 } else {
291 scalar(value.len() as u64, output)?;
292 }
293 output.append(value.as_bytes())?;
294 }
295 Kind::Bytes => {
296 let value = typed::<BinaryArray>(array)?.value(row);
297 if protobuf {
298 header(field.tag, value.len(), output)?;
299 } else {
300 scalar(value.len() as u64, output)?;
301 }
302 output.append(value)?;
303 }
304 Kind::Message { definition } => {
305 let values = struct_column(array)?;
306 if protobuf {
307 let mut count = Counter { bytes: 0, limit: usize::MAX };
308 encode_message(schema, definition, values, row, encoding, depth + 1, &mut count)?;
309 header(field.tag, count.bytes, output)?;
310 }
311 encode_message(schema, definition, values, row, encoding, depth + 1, output)?;
312 }
313 }
314 Ok(())
315}
316
317fn typed<T: Array + 'static>(array: &dyn Array) -> Result<&T> {
318 array.as_any().downcast_ref().ok_or_else(invalid)
319}
320fn struct_column(array: &dyn Array) -> Result<&StructArray> {
321 typed(array)
322}
323fn optional_id(values: &StructArray, name: &str, row: usize) -> Result<Option<Identifier>> {
324 let Some(column) = values.column_by_name(name) else {
325 return Ok(None);
326 };
327 let strings = typed::<StringArray>(column.as_ref())?;
328 if strings.is_null(row) {
329 return Ok(None);
330 }
331 Identifier::new(strings.value(row)).map(Some).map_err(|_| invalid())
332}
333fn required_id(values: &StructArray, name: &str, row: usize) -> Result<Identifier> {
334 optional_id(values, name, row)?.ok_or_else(invalid)
335}
336fn varint(mut value: u64, output: &mut impl Sink) -> Result<()> {
337 let mut bytes = [0u8; 10];
338 let mut count = 0;
339 while value >= 128 {
340 bytes[count] = (value as u8 & 127) | 128;
341 value >>= 7;
342 count += 1;
343 }
344 bytes[count] = value as u8;
345 output.append(&bytes[..=count])
346}
347fn header(tag: u32, length: usize, output: &mut impl Sink) -> Result<()> {
348 varint((u64::from(tag) << 3) | 2, output)?;
349 varint(length as u64, output)
350}
351fn exhausted() -> Error {
352 Error::ResourceExhausted { resource: "Arrow payload encoding bytes" }
353}
354fn invalid() -> Error {
355 Error::InvalidInput { field: "write Arrow batch", reason: "batch metadata or typed payload does not match its schema" }
356}
357
358#[cfg(test)]
361mod tests {
362 use super::*;
363 use arrow_array::{
364 ArrayRef,
365 builder::{ListBuilder, UInt64Builder},
366 };
367 use arrow_schema::{DataType, Field as ArrowField};
368 use std::sync::Arc;
369
370 fn field(name: &str, tag: u32, kind: Kind, cardinality: Cardinality) -> Field {
371 Field { name: name.to_owned().into(), tag, kind, cardinality }
372 }
373
374 fn schema() -> Schema {
375 Schema {
376 root: 0,
377 definitions: vec![
378 vec![
379 field("text", 1, Kind::String, Cardinality::Required),
380 field("bytes", 2, Kind::Bytes, Cardinality::Required),
381 field("values", 3, Kind::U64, Cardinality::Repeated),
382 field("child", 4, Kind::Message { definition: 1 }, Cardinality::Required),
383 ],
384 vec![field("left", 1, Kind::U32, Cardinality::Variant), field("right", 2, Kind::String, Cardinality::Variant)],
385 ],
386 }
387 }
388
389 fn payload(conflict: bool) -> StructArray {
390 let mut lists = ListBuilder::new(UInt64Builder::new());
391 lists.values().append_value(1);
392 lists.values().append_value(300);
393 lists.append(true);
394 lists.append(true);
395 let lists = lists.finish();
396 let child = StructArray::from(vec![
397 (Arc::new(ArrowField::new("__variant", DataType::UInt32, false)), Arc::new(UInt32Array::from(vec![0, 1])) as ArrayRef),
398 (Arc::new(ArrowField::new("left", DataType::UInt32, true)), Arc::new(UInt32Array::from(vec![Some(7), None])) as ArrayRef),
399 (
400 Arc::new(ArrowField::new("right", DataType::Utf8, true)),
401 Arc::new(StringArray::from(vec![if conflict { Some("both") } else { None }, Some("chosen")])) as ArrayRef,
402 ),
403 ]);
404 StructArray::from(vec![
405 (Arc::new(ArrowField::new("text", DataType::Utf8, false)), Arc::new(StringArray::from(vec!["", "payload"])) as ArrayRef),
406 (
407 Arc::new(ArrowField::new("bytes", DataType::Binary, false)),
408 Arc::new(BinaryArray::from(vec![b"".as_slice(), &[0u8, 1, 255]])) as ArrayRef,
409 ),
410 (Arc::new(ArrowField::new("values", lists.data_type().clone(), false)), Arc::new(lists) as ArrayRef),
411 (Arc::new(ArrowField::new("child", child.data_type().clone(), false)), Arc::new(child) as ArrayRef),
412 ])
413 }
414
415 fn resident(rows: &[Vec<u8>], capacity: usize) -> usize {
416 std::mem::size_of::<Vec<Vec<u8>>>() + capacity * std::mem::size_of::<Vec<u8>>() + rows.iter().map(Vec::capacity).sum::<usize>()
417 }
418 #[test]
419 fn payload_bound_capacities() {
420 let schema = schema();
421 let payload = payload(false);
422 for encoding in [PayloadEncoding::PostcardV1, PayloadEncoding::ProtobufV1] {
423 let maximum = NonZeroUsize::new(1_000_000).unwrap();
424 let bound = encode_payloads_memory(&schema, &payload, encoding, maximum).unwrap();
425 let exact = NonZeroUsize::new(bound).unwrap();
426 let rows = encode_payloads(&schema, &payload, encoding, exact).unwrap();
427 assert_eq!(rows.len(), 2);
428 assert!(resident(&rows, rows.capacity()) <= bound);
429 assert_eq!(encode_payloads_memory(&schema, &payload, encoding, exact).unwrap(), bound);
430 let smaller = NonZeroUsize::new(bound - 1).unwrap();
431 assert!(matches!(encode_payloads_memory(&schema, &payload, encoding, smaller), Err(Error::ResourceExhausted { .. })));
432 assert!(matches!(encode_payloads(&schema, &payload, encoding, smaller), Err(Error::ResourceExhausted { .. })));
433 }
434 }
435
436 #[test]
437 fn payload_bound_empty() {
438 let schema = Schema { root: 0, definitions: vec![vec![]] };
439 let payload = StructArray::new_empty_fields(0, None);
440 for encoding in [PayloadEncoding::PostcardV1, PayloadEncoding::ProtobufV1] {
441 let maximum = NonZeroUsize::new(1024).unwrap();
442 let bound = encode_payloads_memory(&schema, &payload, encoding, maximum).unwrap();
443 let rows = encode_payloads(&schema, &payload, encoding, maximum).unwrap();
444 assert!(rows.is_empty());
445 assert_eq!(bound, resident(&rows, rows.capacity()));
446 }
447 }
448
449 #[test]
450 fn payload_bound_malformed() {
451 let schema = schema();
452 let payload = payload(true);
453 for encoding in [PayloadEncoding::PostcardV1, PayloadEncoding::ProtobufV1] {
454 let maximum = NonZeroUsize::new(1_000_000).unwrap();
455 assert!(matches!(encode_payloads_memory(&schema, &payload, encoding, maximum), Err(Error::InvalidInput { .. })));
456 assert!(matches!(encode_payloads(&schema, &payload, encoding, maximum), Err(Error::InvalidInput { .. })));
457 }
458 }
459
460 #[test]
461 fn payload_bound_overflow() {
462 let schema = Schema { root: 0, definitions: vec![vec![]] };
463 let payload = StructArray::new_empty_fields(usize::MAX, None);
465 let maximum = NonZeroUsize::new(usize::MAX).unwrap();
466 for encoding in [PayloadEncoding::PostcardV1, PayloadEncoding::ProtobufV1] {
467 assert!(matches!(encode_payloads_memory(&schema, &payload, encoding, maximum), Err(Error::ResourceExhausted { .. })));
468 assert!(matches!(encode_payloads(&schema, &payload, encoding, maximum), Err(Error::ResourceExhausted { .. })));
469 }
470 }
471}