1use crate::meta::{ColumnId, PredicateValue};
4
5use super::{DocId, DocumentVersion, IngestDocument, Revision};
6
7pub const UPSERT_V1: u16 = 1;
32pub const DELETE_V1: u16 = 2;
34pub const METADATA_EDIT_V1: u16 = 3;
36pub const UPSERT_WITH_TIMESTAMP_V1: u16 = 4;
38pub const UPSERT_WITH_METADATA_V1: u16 = 5;
40pub const UPSERT_WITH_TIMESTAMP_AND_METADATA_V1: u16 = 6;
42pub const UPSERT_V2: u16 = 7;
44pub const UPSERT_V2_BATCH_MEMBER: u16 = 8;
48
49pub const GRAPH_COMMIT_V1: u16 = 10;
51pub const MIXED_BATCH_MEMBER_V1: u16 = 11;
54
55pub const MIXED_BATCH_MEMBER_FRAMED_OVERHEAD: usize = 10 + crate::wal::record::MIN_RECORD_LEN;
57
58pub fn mixed_batch_group_bytes(
62 header_bytes: usize,
63 inner_payload_bytes: &[usize],
64 max_group_bytes: usize,
65) -> Result<usize, crate::wal::WalWriteError> {
66 let encoded_bytes = inner_payload_bytes
67 .iter()
68 .try_fold(header_bytes, |sum, length| {
69 sum.checked_add(*length)?
70 .checked_add(MIXED_BATCH_MEMBER_FRAMED_OVERHEAD)
71 });
72 match encoded_bytes {
73 Some(encoded_bytes) if encoded_bytes <= max_group_bytes => Ok(encoded_bytes),
74 encoded_bytes => Err(crate::wal::WalWriteError::GroupTooLarge {
75 encoded_bytes: encoded_bytes.unwrap_or(usize::MAX),
76 max_group_bytes,
77 }),
78 }
79}
80
81pub const UPSERT_V2_VECTOR: u32 = 1 << 0;
83pub const UPSERT_V2_TEXT: u32 = 1 << 1;
85pub const UPSERT_V2_TIMESTAMP: u32 = 1 << 2;
87pub const UPSERT_V2_METADATA: u32 = 1 << 3;
89pub const UPSERT_V2_TYPED_COLUMNS: u32 = 1 << 4;
91pub const UPSERT_V2_EPOCH_TAG_RESERVED: u32 = 1 << 5;
93
94const UPSERT_V2_KNOWN_FIELDS: u32 = UPSERT_V2_VECTOR
95 | UPSERT_V2_TEXT
96 | UPSERT_V2_TIMESTAMP
97 | UPSERT_V2_METADATA
98 | UPSERT_V2_TYPED_COLUMNS;
99
100const PAYLOAD_VERSION: u16 = 1;
101const COMMON_HEADER_LEN: usize = 4;
102const DOCUMENT_VERSION_LEN: usize = 24;
103const UPSERT_PREFIX_LEN: usize = COMMON_HEADER_LEN + DOCUMENT_VERSION_LEN + 4;
104const TIMESTAMPED_UPSERT_PREFIX_LEN: usize = COMMON_HEADER_LEN + DOCUMENT_VERSION_LEN + 8 + 4;
105const METADATA_UPSERT_PREFIX_LEN: usize = COMMON_HEADER_LEN + DOCUMENT_VERSION_LEN + 4 + 4;
106const TIMESTAMPED_METADATA_UPSERT_PREFIX_LEN: usize =
107 COMMON_HEADER_LEN + DOCUMENT_VERSION_LEN + 8 + 4 + 4;
108const DELETE_PREFIX_LEN: usize = COMMON_HEADER_LEN + 4;
109const METADATA_PREFIX_LEN: usize = COMMON_HEADER_LEN + DOCUMENT_VERSION_LEN + 4 + 1 + 3 + 4;
110
111const VALUE_NULL: u8 = 0;
112const VALUE_ID128: u8 = 6;
113const VALUE_U64: u8 = 1;
114const VALUE_I64: u8 = 2;
115const VALUE_F64: u8 = 3;
116const VALUE_BOOL: u8 = 4;
117const VALUE_STRING: u8 = 5;
118
119#[derive(Clone, Debug, Eq, PartialEq)]
121pub enum PayloadError {
122 #[cfg(feature = "graph-cypher")]
124 GraphEnvelope(crate::property_graph::wal::WalError),
125 UnknownOperation(u16),
127 TransactionBinding,
129 LengthOverflow,
131 Truncated,
133 Version(u16),
135 Flags(u16),
137 Reserved(u32),
139 EmptyVector,
141 EmptyDelete,
143 NonFiniteVector {
145 index: usize,
147 },
148 ValueKind(u8),
150 ValueLength {
152 kind: u8,
154 expected: usize,
156 actual: usize,
158 },
159 Boolean(u8),
161 Utf8,
163 TrailingBytes(usize),
165 FieldBitmap(u32),
167 Lexical(String),
169 Columns(String),
171 BatchPosition {
173 index: u32,
175 count: u32,
177 },
178 OrphanBatchMember {
180 index: u32,
182 count: u32,
184 },
185}
186
187impl std::fmt::Display for PayloadError {
188 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 match self {
190 #[cfg(feature = "graph-cypher")]
191 Self::GraphEnvelope(error) => write!(formatter, "WAL graph commit: {error}"),
192 Self::TransactionBinding => write!(formatter, "invalid transaction binding"),
193 Self::UnknownOperation(op) => {
194 write!(formatter, "WAL mutation operation {op} is unknown")
195 }
196 Self::LengthOverflow => formatter.write_str("WAL mutation payload length overflow"),
197 Self::Truncated => formatter.write_str("WAL mutation payload is truncated"),
198 Self::Version(version) => write!(
199 formatter,
200 "WAL mutation payload version {version} is unsupported"
201 ),
202 Self::Flags(flags) => write!(
203 formatter,
204 "WAL mutation payload flags {flags:#06x} are non-zero"
205 ),
206 Self::Reserved(value) => write!(
207 formatter,
208 "WAL metadata payload reserved bytes are {value:#08x}, expected zero"
209 ),
210 Self::EmptyVector => formatter.write_str("WAL upsert vector is empty"),
211 Self::EmptyDelete => formatter.write_str("WAL delete document list is empty"),
212 Self::NonFiniteVector { index } => write!(
213 formatter,
214 "WAL upsert vector coordinate {index} is non-finite"
215 ),
216 Self::ValueKind(kind) => {
217 write!(formatter, "WAL metadata value kind {kind} is unknown")
218 }
219 Self::ValueLength {
220 kind,
221 expected,
222 actual,
223 } => write!(
224 formatter,
225 "WAL metadata value kind {kind} needs {expected} bytes, got {actual}"
226 ),
227 Self::Boolean(value) => write!(
228 formatter,
229 "WAL metadata Boolean byte {value} is not canonical zero or one"
230 ),
231 Self::Utf8 => formatter.write_str("WAL metadata string is not valid UTF-8"),
232 Self::TrailingBytes(bytes) => {
233 write!(formatter, "WAL mutation payload has {bytes} trailing bytes")
234 }
235 Self::FieldBitmap(bitmap) => write!(
236 formatter,
237 "WAL upsert-v2 field bitmap {bitmap:#010x} names reserved fields"
238 ),
239 Self::Lexical(detail) => write!(formatter, "WAL lexical replay failed: {detail}"),
240 Self::Columns(detail) => write!(formatter, "WAL column replay failed: {detail}"),
241 Self::BatchPosition { index, count } => write!(
242 formatter,
243 "WAL batch member {index} of {count} is not a valid position"
244 ),
245 Self::OrphanBatchMember { index, count } => write!(
246 formatter,
247 "WAL batch member {index} of {count} continues no earlier member"
248 ),
249 }
250 }
251}
252
253impl std::error::Error for PayloadError {}
254
255#[derive(Clone, Debug, PartialEq)]
257pub enum MetadataValue {
258 Null,
260 U64(u64),
262 Id128(DocId),
264 I64(i64),
266 F64(f64),
268 Bool(bool),
270 String(String),
272}
273
274#[derive(Clone, Debug, PartialEq)]
276pub struct MetadataEdit {
277 version: DocumentVersion,
278 column: ColumnId,
279 value: MetadataValue,
280}
281
282impl MetadataEdit {
283 #[must_use]
285 pub const fn new(version: DocumentVersion, column: ColumnId, value: MetadataValue) -> Self {
286 Self {
287 version,
288 column,
289 value,
290 }
291 }
292
293 #[must_use]
295 pub const fn version(&self) -> DocumentVersion {
296 self.version
297 }
298
299 #[must_use]
301 pub const fn column(&self) -> ColumnId {
302 self.column
303 }
304
305 #[must_use]
307 pub const fn value(&self) -> &MetadataValue {
308 &self.value
309 }
310}
311
312#[derive(Clone, Debug, PartialEq)]
314pub enum MutationPayload {
315 #[cfg(feature = "graph-cypher")]
317 GraphCommit(Vec<u8>),
318 MixedBatchMember {
320 index: u32,
322 count: u32,
324 op: u16,
326 mutation: Box<MutationPayload>,
328 },
329 Upsert(IngestDocument),
331 Delete(Vec<DocId>),
333 MetadataEdit(MetadataEdit),
335 BatchMember {
337 index: u32,
339 count: u32,
341 document: IngestDocument,
343 },
344}
345
346pub fn encode_upsert(document: &IngestDocument) -> Result<Vec<u8>, PayloadError> {
349 encode_upsert_body(document, None, false)
350}
351
352pub fn encode_upsert_with_timestamp(document: &IngestDocument) -> Result<Vec<u8>, PayloadError> {
354 encode_upsert_body(document, Some(document.timestamp()), false)
355}
356
357pub fn encode_upsert_with_metadata(document: &IngestDocument) -> Result<Vec<u8>, PayloadError> {
359 encode_upsert_body(document, None, true)
360}
361
362pub fn encode_upsert_with_timestamp_and_metadata(
364 document: &IngestDocument,
365) -> Result<Vec<u8>, PayloadError> {
366 encode_upsert_body(document, Some(document.timestamp()), true)
367}
368
369pub fn encode_upsert_v2(document: &IngestDocument) -> Result<Vec<u8>, PayloadError> {
372 let mut bitmap = UPSERT_V2_VECTOR;
373 if document.text().is_some() {
374 bitmap |= UPSERT_V2_TEXT;
375 }
376 if document.has_timestamp() {
377 bitmap |= UPSERT_V2_TIMESTAMP;
378 }
379 if document.has_metadata() {
380 bitmap |= UPSERT_V2_METADATA;
381 }
382 if document.has_columns() {
383 bitmap |= UPSERT_V2_TYPED_COLUMNS;
384 }
385 validate_vector(document.vector())?;
386 let dims = u32::try_from(document.vector().len()).map_err(|_| PayloadError::LengthOverflow)?;
387 let mut payload = Vec::new();
388 payload.extend_from_slice(&bitmap.to_le_bytes());
389 append_version(&mut payload, document.version());
390 payload.extend_from_slice(&dims.to_le_bytes());
391 for value in document.vector() {
392 payload.extend_from_slice(&value.to_bits().to_le_bytes());
393 }
394 if let Some(text) = document.text() {
395 append_length_prefixed(text.as_bytes(), &mut payload)?;
396 }
397 if document.has_timestamp() {
398 payload.extend_from_slice(&document.timestamp().to_le_bytes());
399 }
400 if document.has_metadata() {
401 append_length_prefixed(document.metadata(), &mut payload)?;
402 }
403 if document.has_columns() {
404 encode_typed_columns(document.columns(), &mut payload)?;
405 }
406 Ok(payload)
407}
408
409fn validate_vector(vector: &[f32]) -> Result<(), PayloadError> {
410 if vector.is_empty() {
411 return Err(PayloadError::EmptyVector);
412 }
413 if let Some(index) = vector.iter().position(|value| !value.is_finite()) {
414 return Err(PayloadError::NonFiniteVector { index });
415 }
416 Ok(())
417}
418
419fn append_length_prefixed(bytes: &[u8], output: &mut Vec<u8>) -> Result<(), PayloadError> {
420 let length = u32::try_from(bytes.len()).map_err(|_| PayloadError::LengthOverflow)?;
421 output.extend_from_slice(&length.to_le_bytes());
422 output.extend_from_slice(bytes);
423 Ok(())
424}
425
426fn encode_typed_columns(
427 columns: &[(ColumnId, PredicateValue)],
428 output: &mut Vec<u8>,
429) -> Result<(), PayloadError> {
430 let count = u32::try_from(columns.len()).map_err(|_| PayloadError::LengthOverflow)?;
431 output.extend_from_slice(&count.to_le_bytes());
432 for (column, value) in columns {
433 let metadata = predicate_to_metadata(value);
434 let (kind, body) = encode_metadata_value(&metadata)?;
435 output.extend_from_slice(&column.get().to_le_bytes());
436 output.push(kind);
437 output.extend_from_slice(&[0_u8; 3]);
438 append_length_prefixed(&body, output)?;
439 }
440 Ok(())
441}
442
443pub(crate) fn encode_column_values(
444 columns: &[(ColumnId, PredicateValue)],
445) -> Result<Vec<u8>, PayloadError> {
446 let mut encoded = Vec::new();
447 encode_typed_columns(columns, &mut encoded)?;
448 Ok(encoded)
449}
450
451fn predicate_to_metadata(value: &PredicateValue) -> MetadataValue {
452 match value {
453 PredicateValue::Id128(value) => MetadataValue::Id128(*value),
454 PredicateValue::U64(value) => MetadataValue::U64(*value),
455 PredicateValue::I64(value) => MetadataValue::I64(*value),
456 PredicateValue::F64(value) => MetadataValue::F64(*value),
457 PredicateValue::Bool(value) => MetadataValue::Bool(*value),
458 PredicateValue::String(value) => MetadataValue::String(value.clone()),
459 }
460}
461
462fn encode_upsert_body(
463 document: &IngestDocument,
464 timestamp: Option<i64>,
465 include_metadata: bool,
466) -> Result<Vec<u8>, PayloadError> {
467 if document.vector().is_empty() {
468 return Err(PayloadError::EmptyVector);
469 }
470 let dims = u32::try_from(document.vector().len()).map_err(|_| PayloadError::LengthOverflow)?;
471 if let Some(index) = document
472 .vector()
473 .iter()
474 .position(|value| !value.is_finite())
475 {
476 return Err(PayloadError::NonFiniteVector { index });
477 }
478 let vector_bytes = document
479 .vector()
480 .len()
481 .checked_mul(std::mem::size_of::<f32>())
482 .ok_or(PayloadError::LengthOverflow)?;
483 let prefix = match (timestamp.is_some(), include_metadata) {
484 (false, false) => UPSERT_PREFIX_LEN,
485 (true, false) => TIMESTAMPED_UPSERT_PREFIX_LEN,
486 (false, true) => METADATA_UPSERT_PREFIX_LEN,
487 (true, true) => TIMESTAMPED_METADATA_UPSERT_PREFIX_LEN,
488 };
489 let capacity = prefix
490 .checked_add(if include_metadata {
491 document.metadata().len()
492 } else {
493 0
494 })
495 .and_then(|value| value.checked_add(vector_bytes))
496 .ok_or(PayloadError::LengthOverflow)?;
497 let mut payload = Vec::with_capacity(capacity);
498 append_header(&mut payload);
499 append_version(&mut payload, document.version());
500 if let Some(timestamp) = timestamp {
501 payload.extend_from_slice(×tamp.to_le_bytes());
502 }
503 if include_metadata {
504 let metadata_len =
505 u32::try_from(document.metadata().len()).map_err(|_| PayloadError::LengthOverflow)?;
506 payload.extend_from_slice(&metadata_len.to_le_bytes());
507 }
508 payload.extend_from_slice(&dims.to_le_bytes());
509 if include_metadata {
510 payload.extend_from_slice(document.metadata());
511 }
512 for value in document.vector() {
513 payload.extend_from_slice(&value.to_bits().to_le_bytes());
514 }
515 Ok(payload)
516}
517
518pub fn encode_delete(doc_ids: &[DocId]) -> Result<Vec<u8>, PayloadError> {
521 if doc_ids.is_empty() {
522 return Err(PayloadError::EmptyDelete);
523 }
524 let count = u32::try_from(doc_ids.len()).map_err(|_| PayloadError::LengthOverflow)?;
525 let body_bytes = doc_ids
526 .len()
527 .checked_mul(std::mem::size_of::<u128>())
528 .ok_or(PayloadError::LengthOverflow)?;
529 let capacity = DELETE_PREFIX_LEN
530 .checked_add(body_bytes)
531 .ok_or(PayloadError::LengthOverflow)?;
532 let mut payload = Vec::with_capacity(capacity);
533 append_header(&mut payload);
534 payload.extend_from_slice(&count.to_le_bytes());
535 for doc_id in doc_ids {
536 payload.extend_from_slice(&doc_id.get().to_le_bytes());
537 }
538 Ok(payload)
539}
540
541pub fn encode_metadata_edit(edit: &MetadataEdit) -> Result<Vec<u8>, PayloadError> {
544 let (kind, body) = encode_metadata_value(edit.value())?;
545 let value_len = u32::try_from(body.len()).map_err(|_| PayloadError::LengthOverflow)?;
546 let capacity = METADATA_PREFIX_LEN
547 .checked_add(body.len())
548 .ok_or(PayloadError::LengthOverflow)?;
549 let mut payload = Vec::with_capacity(capacity);
550 append_header(&mut payload);
551 append_version(&mut payload, edit.version());
552 payload.extend_from_slice(&edit.column().get().to_le_bytes());
553 payload.push(kind);
554 payload.extend_from_slice(&[0_u8; 3]);
555 payload.extend_from_slice(&value_len.to_le_bytes());
556 payload.extend_from_slice(&body);
557 Ok(payload)
558}
559
560pub fn decode_mutation(op: u16, payload: &[u8]) -> Result<MutationPayload, PayloadError> {
562 match op {
563 MIXED_BATCH_MEMBER_V1 => decode_mixed_batch_member(payload),
564 #[cfg(feature = "graph-cypher")]
565 GRAPH_COMMIT_V1 => decode_graph_commit(payload).map(MutationPayload::GraphCommit),
566 UPSERT_V1 => decode_upsert(payload).map(MutationPayload::Upsert),
567 DELETE_V1 => decode_delete(payload).map(MutationPayload::Delete),
568 METADATA_EDIT_V1 => decode_metadata_edit(payload).map(MutationPayload::MetadataEdit),
569 UPSERT_WITH_TIMESTAMP_V1 => {
570 decode_upsert_with_timestamp(payload).map(MutationPayload::Upsert)
571 }
572 UPSERT_WITH_METADATA_V1 => {
573 decode_upsert_with_metadata(payload).map(MutationPayload::Upsert)
574 }
575 UPSERT_WITH_TIMESTAMP_AND_METADATA_V1 => {
576 decode_upsert_with_timestamp_and_metadata(payload).map(MutationPayload::Upsert)
577 }
578 UPSERT_V2 => decode_upsert_v2(payload).map(MutationPayload::Upsert),
579 UPSERT_V2_BATCH_MEMBER => {
580 decode_upsert_v2_batch_member(payload).map(|(index, count, document)| {
581 MutationPayload::BatchMember {
582 index,
583 count,
584 document,
585 }
586 })
587 }
588 unknown => Err(PayloadError::UnknownOperation(unknown)),
589 }
590}
591
592pub fn encode_mixed_batch_member(
594 index: u32,
595 count: u32,
596 op: u16,
597 inner: &[u8],
598) -> Result<Vec<u8>, PayloadError> {
599 validate_batch_position(index, count)?;
600 validate_mixed_operation(op)?;
601 let length = inner
602 .len()
603 .checked_add(10)
604 .ok_or(PayloadError::LengthOverflow)?;
605 let mut payload = Vec::new();
606 payload
607 .try_reserve_exact(length)
608 .map_err(|_| PayloadError::LengthOverflow)?;
609 payload.extend_from_slice(&index.to_le_bytes());
610 payload.extend_from_slice(&count.to_le_bytes());
611 payload.extend_from_slice(&op.to_le_bytes());
612 payload.extend_from_slice(inner);
613 Ok(payload)
614}
615
616pub fn decode_mixed_batch_member(payload: &[u8]) -> Result<MutationPayload, PayloadError> {
618 let mut cursor = Cursor::new(payload);
619 let index = cursor.read_u32()?;
620 let count = cursor.read_u32()?;
621 validate_batch_position(index, count)?;
622 let op = cursor.read_u16()?;
623 validate_mixed_operation(op)?;
624 let inner = payload.get(10..).ok_or(PayloadError::Truncated)?;
625 let mutation = Box::new(decode_mutation(op, inner)?);
626 Ok(MutationPayload::MixedBatchMember {
627 index,
628 count,
629 op,
630 mutation,
631 })
632}
633
634fn validate_mixed_operation(op: u16) -> Result<(), PayloadError> {
635 if !matches!(op, DELETE_V1 | UPSERT_V2 | GRAPH_COMMIT_V1) {
636 return Err(PayloadError::UnknownOperation(op));
637 }
638 Ok(())
639}
640
641#[cfg(feature = "graph-cypher")]
644pub fn encode_graph_commit(envelope: &[u8]) -> Result<Vec<u8>, PayloadError> {
645 decode_graph_commit(envelope)
646}
647
648#[cfg(feature = "graph-cypher")]
651pub fn decode_graph_commit(payload: &[u8]) -> Result<Vec<u8>, PayloadError> {
652 use crate::property_graph::wal::{
653 STACK_RESERVATION_BYTES, WalResources, validate_envelope_framing,
654 };
655 let mut cancelled = || false;
658 let mut resources = WalResources::new(
659 64 * crate::property_graph::wal::MAX_ENVELOPE_BYTES as u64,
660 STACK_RESERVATION_BYTES,
661 &mut cancelled,
662 )
663 .map_err(PayloadError::GraphEnvelope)?;
664 validate_envelope_framing(payload, &mut resources).map_err(PayloadError::GraphEnvelope)?;
665 let mut bytes = Vec::new();
666 bytes
667 .try_reserve_exact(payload.len())
668 .map_err(|_| PayloadError::LengthOverflow)?;
669 bytes.extend_from_slice(payload);
670 Ok(bytes)
671}
672
673pub fn encode_upsert_v2_batch_member(
675 index: u32,
676 count: u32,
677 upsert_v2: &[u8],
678) -> Result<Vec<u8>, PayloadError> {
679 validate_batch_position(index, count)?;
680 let mut payload = Vec::new();
681 payload
682 .try_reserve_exact(upsert_v2.len().saturating_add(8))
683 .map_err(|_| PayloadError::LengthOverflow)?;
684 payload.extend_from_slice(&index.to_le_bytes());
685 payload.extend_from_slice(&count.to_le_bytes());
686 payload.extend_from_slice(upsert_v2);
687 Ok(payload)
688}
689
690pub fn decode_upsert_v2_batch_member(
692 payload: &[u8],
693) -> Result<(u32, u32, IngestDocument), PayloadError> {
694 let mut cursor = Cursor::new(payload);
695 let index = cursor.read_u32()?;
696 let count = cursor.read_u32()?;
697 validate_batch_position(index, count)?;
698 let body = payload.get(8..).ok_or(PayloadError::Truncated)?;
699 decode_upsert_v2(body).map(|document| (index, count, document))
700}
701
702fn validate_batch_position(index: u32, count: u32) -> Result<(), PayloadError> {
703 if count < 2 || index >= count {
704 return Err(PayloadError::BatchPosition { index, count });
705 }
706 Ok(())
707}
708
709pub fn decode_upsert_v2(payload: &[u8]) -> Result<IngestDocument, PayloadError> {
711 let mut cursor = Cursor::new(payload);
712 let bitmap = cursor.read_u32()?;
713 let reserved = bitmap & !UPSERT_V2_KNOWN_FIELDS;
714 if reserved != 0 {
715 return Err(PayloadError::FieldBitmap(reserved));
716 }
717 let version = cursor.read_version()?;
718 let vector = if bitmap & UPSERT_V2_VECTOR != 0 {
719 let dims = usize::try_from(cursor.read_u32()?).map_err(|_| PayloadError::LengthOverflow)?;
720 if dims == 0 {
721 return Err(PayloadError::EmptyVector);
722 }
723 let mut vector = Vec::with_capacity(dims);
724 for index in 0..dims {
725 let value = f32::from_bits(cursor.read_u32()?);
726 if !value.is_finite() {
727 return Err(PayloadError::NonFiniteVector { index });
728 }
729 vector.push(value);
730 }
731 vector
732 } else {
733 Vec::new()
734 };
735 let text = if bitmap & UPSERT_V2_TEXT != 0 {
736 Some(cursor.read_string()?)
737 } else {
738 None
739 };
740 let timestamp = if bitmap & UPSERT_V2_TIMESTAMP != 0 {
741 Some(i64::from_le_bytes(cursor.take()?))
742 } else {
743 None
744 };
745 let metadata = if bitmap & UPSERT_V2_METADATA != 0 {
746 Some(cursor.read_length_prefixed()?.to_vec())
747 } else {
748 None
749 };
750 let columns = if bitmap & UPSERT_V2_TYPED_COLUMNS != 0 {
751 Some(decode_typed_columns(&mut cursor)?)
752 } else {
753 None
754 };
755 cursor.finish()?;
756 let mut document = IngestDocument::new(version, vector);
757 if let Some(text) = text {
758 document = document.with_text(text);
759 }
760 if let Some(timestamp) = timestamp {
761 document = document.with_timestamp(timestamp);
762 }
763 if let Some(metadata) = metadata {
764 document = document.with_metadata(metadata);
765 }
766 if let Some(columns) = columns {
767 document = document.with_columns(columns);
768 }
769 Ok(document)
770}
771
772fn decode_typed_columns(
773 cursor: &mut Cursor<'_>,
774) -> Result<Vec<(ColumnId, PredicateValue)>, PayloadError> {
775 let count = usize::try_from(cursor.read_u32()?).map_err(|_| PayloadError::LengthOverflow)?;
776 let mut columns = Vec::with_capacity(count);
777 for _ in 0..count {
778 let column = ColumnId::new(cursor.read_u32()?);
779 let kind = cursor.read_u8()?;
780 let reserved = cursor.read_u24()?;
781 if reserved != 0 {
782 return Err(PayloadError::Reserved(reserved));
783 }
784 let body = cursor.read_length_prefixed()?;
785 let value = metadata_to_predicate(decode_metadata_value(kind, body)?)?;
786 columns.push((column, value));
787 }
788 Ok(columns)
789}
790
791pub(crate) fn decode_column_values(
792 encoded: &[u8],
793) -> Result<Vec<(ColumnId, PredicateValue)>, PayloadError> {
794 let mut cursor = Cursor::new(encoded);
795 let columns = decode_typed_columns(&mut cursor)?;
796 cursor.finish()?;
797 Ok(columns)
798}
799
800fn metadata_to_predicate(value: MetadataValue) -> Result<PredicateValue, PayloadError> {
801 match value {
802 MetadataValue::Null => Err(PayloadError::ValueKind(VALUE_NULL)),
803 MetadataValue::Id128(value) => Ok(PredicateValue::Id128(value)),
804 MetadataValue::U64(value) => Ok(PredicateValue::U64(value)),
805 MetadataValue::I64(value) => Ok(PredicateValue::I64(value)),
806 MetadataValue::F64(value) => Ok(PredicateValue::F64(value)),
807 MetadataValue::Bool(value) => Ok(PredicateValue::Bool(value)),
808 MetadataValue::String(value) => Ok(PredicateValue::String(value)),
809 }
810}
811
812pub fn decode_upsert(payload: &[u8]) -> Result<IngestDocument, PayloadError> {
814 decode_upsert_body(payload, false, false)
815}
816
817pub fn decode_upsert_with_timestamp(payload: &[u8]) -> Result<IngestDocument, PayloadError> {
819 decode_upsert_body(payload, true, false)
820}
821
822pub fn decode_upsert_with_metadata(payload: &[u8]) -> Result<IngestDocument, PayloadError> {
824 decode_upsert_body(payload, false, true)
825}
826
827pub fn decode_upsert_with_timestamp_and_metadata(
829 payload: &[u8],
830) -> Result<IngestDocument, PayloadError> {
831 decode_upsert_body(payload, true, true)
832}
833
834fn decode_upsert_body(
835 payload: &[u8],
836 has_timestamp: bool,
837 has_metadata: bool,
838) -> Result<IngestDocument, PayloadError> {
839 let mut cursor = Cursor::new(payload);
840 cursor.require_header()?;
841 let version = cursor.read_version()?;
842 let timestamp = if has_timestamp {
843 i64::from_le_bytes(cursor.take()?)
844 } else {
845 0
846 };
847 let metadata_len = if has_metadata {
848 usize::try_from(cursor.read_u32()?).map_err(|_| PayloadError::LengthOverflow)?
849 } else {
850 0
851 };
852 let dims = usize::try_from(cursor.read_u32()?).map_err(|_| PayloadError::LengthOverflow)?;
853 if dims == 0 {
854 return Err(PayloadError::EmptyVector);
855 }
856 let body_bytes = dims
857 .checked_mul(std::mem::size_of::<f32>())
858 .ok_or(PayloadError::LengthOverflow)?;
859 let remaining = metadata_len
860 .checked_add(body_bytes)
861 .ok_or(PayloadError::LengthOverflow)?;
862 cursor.require_remaining(remaining)?;
863 let metadata = cursor.read_bytes(metadata_len)?.to_vec();
864 let mut vector = Vec::with_capacity(dims);
865 for index in 0..dims {
866 let value = f32::from_bits(cursor.read_u32()?);
867 if !value.is_finite() {
868 return Err(PayloadError::NonFiniteVector { index });
869 }
870 vector.push(value);
871 }
872 cursor.finish()?;
873 let mut document = IngestDocument::new(version, vector);
874 if has_timestamp {
875 document = document.with_timestamp(timestamp);
876 }
877 if has_metadata {
878 document = document.with_metadata(metadata);
879 }
880 Ok(document)
881}
882
883pub fn decode_delete(payload: &[u8]) -> Result<Vec<DocId>, PayloadError> {
885 let mut cursor = Cursor::new(payload);
886 cursor.require_header()?;
887 let count = usize::try_from(cursor.read_u32()?).map_err(|_| PayloadError::LengthOverflow)?;
888 if count == 0 {
889 return Err(PayloadError::EmptyDelete);
890 }
891 let body_bytes = count
892 .checked_mul(std::mem::size_of::<u128>())
893 .ok_or(PayloadError::LengthOverflow)?;
894 cursor.require_remaining(body_bytes)?;
895 let mut doc_ids = Vec::with_capacity(count);
896 for _ in 0..count {
897 doc_ids.push(DocId::new(cursor.read_u128()?));
898 }
899 cursor.finish()?;
900 Ok(doc_ids)
901}
902
903pub fn decode_metadata_edit(payload: &[u8]) -> Result<MetadataEdit, PayloadError> {
905 let mut cursor = Cursor::new(payload);
906 cursor.require_header()?;
907 let version = cursor.read_version()?;
908 let column = ColumnId::new(cursor.read_u32()?);
909 let kind = cursor.read_u8()?;
910 let reserved = cursor.read_u24()?;
911 if reserved != 0 {
912 return Err(PayloadError::Reserved(reserved));
913 }
914 let value_len =
915 usize::try_from(cursor.read_u32()?).map_err(|_| PayloadError::LengthOverflow)?;
916 let body = cursor.read_bytes(value_len)?;
917 let value = decode_metadata_value(kind, body)?;
918 cursor.finish()?;
919 Ok(MetadataEdit::new(version, column, value))
920}
921
922fn append_header(payload: &mut Vec<u8>) {
923 payload.extend_from_slice(&PAYLOAD_VERSION.to_le_bytes());
924 payload.extend_from_slice(&0_u16.to_le_bytes());
925}
926
927fn append_version(payload: &mut Vec<u8>, version: DocumentVersion) {
928 payload.extend_from_slice(&version.doc_id().get().to_le_bytes());
929 payload.extend_from_slice(&version.revision().get().to_le_bytes());
930}
931
932fn encode_metadata_value(value: &MetadataValue) -> Result<(u8, Vec<u8>), PayloadError> {
933 let encoded = match value {
934 MetadataValue::Null => (VALUE_NULL, Vec::new()),
935 MetadataValue::Id128(value) => (VALUE_ID128, value.get().to_le_bytes().to_vec()),
936 MetadataValue::U64(value) => (VALUE_U64, value.to_le_bytes().to_vec()),
937 MetadataValue::I64(value) => (VALUE_I64, value.to_le_bytes().to_vec()),
938 MetadataValue::F64(value) => (VALUE_F64, value.to_bits().to_le_bytes().to_vec()),
939 MetadataValue::Bool(value) => (VALUE_BOOL, vec![u8::from(*value)]),
940 MetadataValue::String(value) => {
941 u32::try_from(value.len()).map_err(|_| PayloadError::LengthOverflow)?;
942 (VALUE_STRING, value.as_bytes().to_vec())
943 }
944 };
945 Ok(encoded)
946}
947
948fn decode_metadata_value(kind: u8, body: &[u8]) -> Result<MetadataValue, PayloadError> {
949 match kind {
950 VALUE_NULL => {
951 require_value_len(kind, body, 0)?;
952 Ok(MetadataValue::Null)
953 }
954 VALUE_ID128 => {
955 require_value_len(kind, body, 16)?;
956 read_array(body)
957 .map(u128::from_le_bytes)
958 .map(DocId::new)
959 .map(MetadataValue::Id128)
960 }
961 VALUE_U64 => {
962 require_value_len(kind, body, 8)?;
963 read_array(body)
964 .map(u64::from_le_bytes)
965 .map(MetadataValue::U64)
966 }
967 VALUE_I64 => {
968 require_value_len(kind, body, 8)?;
969 read_array(body)
970 .map(i64::from_le_bytes)
971 .map(MetadataValue::I64)
972 }
973 VALUE_F64 => {
974 require_value_len(kind, body, 8)?;
975 read_array(body)
976 .map(u64::from_le_bytes)
977 .map(f64::from_bits)
978 .map(MetadataValue::F64)
979 }
980 VALUE_BOOL => {
981 require_value_len(kind, body, 1)?;
982 match body.first().copied().ok_or(PayloadError::Truncated)? {
983 0 => Ok(MetadataValue::Bool(false)),
984 1 => Ok(MetadataValue::Bool(true)),
985 value => Err(PayloadError::Boolean(value)),
986 }
987 }
988 VALUE_STRING => std::str::from_utf8(body)
989 .map(|value| MetadataValue::String(value.to_owned()))
990 .map_err(|_| PayloadError::Utf8),
991 unknown => Err(PayloadError::ValueKind(unknown)),
992 }
993}
994
995fn require_value_len(kind: u8, body: &[u8], expected: usize) -> Result<(), PayloadError> {
996 if body.len() == expected {
997 Ok(())
998 } else {
999 Err(PayloadError::ValueLength {
1000 kind,
1001 expected,
1002 actual: body.len(),
1003 })
1004 }
1005}
1006
1007fn read_array<const N: usize>(bytes: &[u8]) -> Result<[u8; N], PayloadError> {
1008 <[u8; N]>::try_from(bytes).map_err(|_| PayloadError::Truncated)
1009}
1010
1011struct Cursor<'a> {
1012 bytes: &'a [u8],
1013 offset: usize,
1014}
1015
1016impl<'a> Cursor<'a> {
1017 const fn new(bytes: &'a [u8]) -> Self {
1018 Self { bytes, offset: 0 }
1019 }
1020
1021 fn require_header(&mut self) -> Result<(), PayloadError> {
1022 let version = self.read_u16()?;
1023 if version != PAYLOAD_VERSION {
1024 return Err(PayloadError::Version(version));
1025 }
1026 let flags = self.read_u16()?;
1027 if flags != 0 {
1028 return Err(PayloadError::Flags(flags));
1029 }
1030 Ok(())
1031 }
1032
1033 fn read_version(&mut self) -> Result<DocumentVersion, PayloadError> {
1034 Ok(DocumentVersion::new(
1035 DocId::new(self.read_u128()?),
1036 Revision::new(self.read_u64()?),
1037 ))
1038 }
1039
1040 fn take<const N: usize>(&mut self) -> Result<[u8; N], PayloadError> {
1041 read_array(self.read_bytes(N)?)
1042 }
1043
1044 fn read_bytes(&mut self, length: usize) -> Result<&'a [u8], PayloadError> {
1045 let end = self
1046 .offset
1047 .checked_add(length)
1048 .ok_or(PayloadError::LengthOverflow)?;
1049 let bytes = self
1050 .bytes
1051 .get(self.offset..end)
1052 .ok_or(PayloadError::Truncated)?;
1053 self.offset = end;
1054 Ok(bytes)
1055 }
1056
1057 fn require_remaining(&self, length: usize) -> Result<(), PayloadError> {
1058 if self.remaining() < length {
1059 Err(PayloadError::Truncated)
1060 } else {
1061 Ok(())
1062 }
1063 }
1064
1065 fn read_u8(&mut self) -> Result<u8, PayloadError> {
1066 self.take::<1>().map(u8::from_le_bytes)
1067 }
1068
1069 fn read_u16(&mut self) -> Result<u16, PayloadError> {
1070 self.take().map(u16::from_le_bytes)
1071 }
1072
1073 fn read_u24(&mut self) -> Result<u32, PayloadError> {
1074 let [low, middle, high] = self.take::<3>()?;
1075 Ok(u32::from(low) | (u32::from(middle) << 8) | (u32::from(high) << 16))
1076 }
1077
1078 fn read_u32(&mut self) -> Result<u32, PayloadError> {
1079 self.take().map(u32::from_le_bytes)
1080 }
1081
1082 fn read_u64(&mut self) -> Result<u64, PayloadError> {
1083 self.take().map(u64::from_le_bytes)
1084 }
1085
1086 fn read_u128(&mut self) -> Result<u128, PayloadError> {
1087 self.take().map(u128::from_le_bytes)
1088 }
1089
1090 fn read_length_prefixed(&mut self) -> Result<&'a [u8], PayloadError> {
1091 let length = usize::try_from(self.read_u32()?).map_err(|_| PayloadError::LengthOverflow)?;
1092 self.read_bytes(length)
1093 }
1094
1095 fn read_string(&mut self) -> Result<String, PayloadError> {
1096 std::str::from_utf8(self.read_length_prefixed()?)
1097 .map(str::to_owned)
1098 .map_err(|_| PayloadError::Utf8)
1099 }
1100
1101 fn remaining(&self) -> usize {
1102 self.bytes.len().saturating_sub(self.offset)
1103 }
1104
1105 fn finish(self) -> Result<(), PayloadError> {
1106 let remaining = self.remaining();
1107 if remaining == 0 {
1108 Ok(())
1109 } else {
1110 Err(PayloadError::TrailingBytes(remaining))
1111 }
1112 }
1113}
1114
1115#[cfg(test)]
1116#[allow(clippy::unwrap_used)]
1117mod id128_tests {
1118 use super::*;
1119
1120 #[test]
1121 fn id128_wal_layout_is_sixteen_little_endian_bytes() {
1122 let value = MetadataValue::Id128(DocId::new(0xffeeddccbbaa99887766554433221100));
1123 let bytes = vec![
1124 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
1125 0xee, 0xff,
1126 ];
1127 assert_eq!(encode_metadata_value(&value).unwrap(), (6, bytes.clone()));
1128 assert_eq!(decode_metadata_value(6, &bytes).unwrap(), value);
1129 for length in [0, 8, 15, 17] {
1130 assert!(decode_metadata_value(6, &vec![0; length]).is_err());
1131 }
1132 }
1133}
1134
1135#[cfg(test)]
1136#[allow(clippy::unwrap_used)]
1137mod transaction_format_tests {
1138 use super::*;
1139 fn binding() -> TransactionBinding {
1140 TransactionBinding {
1141 transaction: 1,
1142 participant: 2,
1143 first_seq: 3,
1144 last_seq: 3,
1145 manifest_digest: 4,
1146 final_generation: 5,
1147 }
1148 }
1149 #[test]
1150 #[allow(clippy::indexing_slicing)]
1151 fn prepared_encoding_golden_and_rejections() {
1152 let body = encode_delete(&[DocId::new(6)]).unwrap();
1153 let bytes = encode_prepared(binding(), 0, 1, DELETE_V1, &body).unwrap();
1154 let hex = bytes.iter().map(|b| format!("{b:02x}")).collect::<String>();
1155 assert_eq!(
1156 hex,
1157 "010000000100000000000000000000000000000002000000000000000000000000000000030000000000000003000000000000000400000000000000050000000000000000000000010000000200010000000100000006000000000000000000000000000000"
1158 );
1159 let decoded = decode_prepared(&bytes).unwrap();
1160 assert_eq!(decoded.binding, binding());
1161 assert_eq!(
1162 decoded.mutation,
1163 MutationPayload::Delete(vec![DocId::new(6)])
1164 );
1165 for end in 0..bytes.len() {
1166 assert!(decode_prepared(&bytes[..end]).is_err(), "{end}");
1167 }
1168 for offset in [0, 2, 4, 20, 36, 60, 68, 72, 76] {
1169 let mut bad = bytes.clone();
1170 bad[offset] = 255;
1171 if matches!(offset, 4 | 20 | 36 | 60) {
1172 if let Ok(member) = decode_prepared(&bad) {
1174 assert_eq!(
1175 transaction_decision(member.binding, Some(binding())),
1176 TransactionDecision::Mismatch
1177 );
1178 }
1179 } else {
1180 assert!(decode_prepared(&bad).is_err(), "{offset}");
1181 }
1182 }
1183 assert!(encode_prepared(binding(), 0, 1, PREPARED_MUTATION_V1, &bytes).is_err());
1184 let mut trailing = bytes.clone();
1185 trailing.push(0);
1186 assert!(decode_prepared(&trailing).is_err());
1187 }
1188 #[test]
1189 fn transaction_decision_table() {
1190 assert_eq!(
1191 transaction_decision(binding(), None),
1192 TransactionDecision::Undecided
1193 );
1194 assert_eq!(
1195 transaction_decision(binding(), Some(binding())),
1196 TransactionDecision::Committed
1197 );
1198 for field in 0..6 {
1199 let mut changed = binding();
1200 match field {
1201 0 => changed.transaction += 1,
1202 1 => changed.participant += 1,
1203 2 => changed.first_seq += 1,
1204 3 => changed.last_seq += 1,
1205 4 => changed.manifest_digest += 1,
1206 _ => changed.final_generation += 1,
1207 }
1208 assert_eq!(
1209 transaction_decision(binding(), Some(changed)),
1210 TransactionDecision::Mismatch
1211 );
1212 }
1213 let mut invalid = binding();
1214 invalid.transaction = 0;
1215 assert_eq!(
1216 transaction_decision(invalid, None),
1217 TransactionDecision::Mismatch
1218 );
1219 }
1220 #[test]
1221 fn prepared_operation_is_recognized() {
1222 let mut bytes = vec![1, 0, 0, 0];
1225 bytes.extend_from_slice(&1_u128.to_le_bytes());
1226 bytes.extend_from_slice(&2_u128.to_le_bytes());
1227 for value in [3_u64, 3, 4, 5] {
1228 bytes.extend_from_slice(&value.to_le_bytes());
1229 }
1230 bytes.extend_from_slice(&0_u32.to_le_bytes());
1231 bytes.extend_from_slice(&1_u32.to_le_bytes());
1232 bytes.extend_from_slice(&2_u16.to_le_bytes());
1233 bytes.extend_from_slice(&super::encode_delete(&[super::DocId::new(6)]).unwrap());
1234 assert!(super::decode_prepared(&bytes).is_ok());
1235 }
1236}
1237
1238pub const PREPARED_MUTATION_V1: u16 = 9;
1240
1241#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1244pub struct TransactionBinding {
1245 pub transaction: u128,
1247 pub participant: u128,
1249 pub first_seq: u64,
1251 pub last_seq: u64,
1253 pub manifest_digest: u64,
1255 pub final_generation: u64,
1257}
1258impl TransactionBinding {
1259 pub fn encode(self) -> Result<Vec<u8>, PayloadError> {
1261 self.member_count()?;
1262 let mut bytes = Vec::with_capacity(64);
1263 bytes.extend_from_slice(&self.transaction.to_le_bytes());
1264 bytes.extend_from_slice(&self.participant.to_le_bytes());
1265 for value in [
1266 self.first_seq,
1267 self.last_seq,
1268 self.manifest_digest,
1269 self.final_generation,
1270 ] {
1271 bytes.extend_from_slice(&value.to_le_bytes());
1272 }
1273 Ok(bytes)
1274 }
1275 pub fn decode(bytes: &[u8]) -> Result<Self, PayloadError> {
1277 let mut cursor = Cursor::new(bytes);
1278 let binding = Self {
1279 transaction: cursor.read_u128()?,
1280 participant: cursor.read_u128()?,
1281 first_seq: cursor.read_u64()?,
1282 last_seq: cursor.read_u64()?,
1283 manifest_digest: cursor.read_u64()?,
1284 final_generation: cursor.read_u64()?,
1285 };
1286 cursor.finish()?;
1287 binding.member_count()?;
1288 Ok(binding)
1289 }
1290 fn member_count(self) -> Result<u32, PayloadError> {
1291 if self.transaction == 0
1292 || self.participant == 0
1293 || self.first_seq == 0
1294 || self.final_generation == 0
1295 {
1296 return Err(PayloadError::TransactionBinding);
1297 }
1298 self.last_seq
1299 .checked_sub(self.first_seq)
1300 .and_then(|n| n.checked_add(1))
1301 .and_then(|n| u32::try_from(n).ok())
1302 .ok_or(PayloadError::TransactionBinding)
1303 }
1304}
1305
1306#[derive(Clone, Debug, PartialEq)]
1308pub struct PreparedMutation {
1309 pub binding: TransactionBinding,
1311 pub index: u32,
1313 pub count: u32,
1315 pub op: u16,
1317 pub mutation: MutationPayload,
1319}
1320
1321pub fn encode_prepared(
1323 binding: TransactionBinding,
1324 index: u32,
1325 count: u32,
1326 op: u16,
1327 body: &[u8],
1328) -> Result<Vec<u8>, PayloadError> {
1329 if count != binding.member_count()? || index >= count {
1330 return Err(PayloadError::BatchPosition { index, count });
1331 }
1332 standalone_mutation(op, body)?;
1333 let mut bytes = Vec::new();
1334 bytes
1335 .try_reserve_exact(
1336 body.len()
1337 .checked_add(78)
1338 .ok_or(PayloadError::LengthOverflow)?,
1339 )
1340 .map_err(|_| PayloadError::LengthOverflow)?;
1341 append_header(&mut bytes);
1342 bytes.extend_from_slice(&binding.encode()?);
1343 bytes.extend_from_slice(&index.to_le_bytes());
1344 bytes.extend_from_slice(&count.to_le_bytes());
1345 bytes.extend_from_slice(&op.to_le_bytes());
1346 bytes.extend_from_slice(body);
1347 Ok(bytes)
1348}
1349
1350pub fn decode_prepared(bytes: &[u8]) -> Result<PreparedMutation, PayloadError> {
1352 let mut cursor = Cursor::new(bytes);
1353 cursor.require_header()?;
1354 let binding = TransactionBinding::decode(cursor.read_bytes(64)?)?;
1355 let index = cursor.read_u32()?;
1356 let count = cursor.read_u32()?;
1357 if count != binding.member_count()? || index >= count {
1358 return Err(PayloadError::BatchPosition { index, count });
1359 }
1360 let op = cursor.read_u16()?;
1361 let mutation = standalone_mutation(op, cursor.read_bytes(cursor.remaining())?)?;
1362 Ok(PreparedMutation {
1363 binding,
1364 index,
1365 count,
1366 op,
1367 mutation,
1368 })
1369}
1370fn standalone_mutation(op: u16, bytes: &[u8]) -> Result<MutationPayload, PayloadError> {
1371 if !(UPSERT_V1..=UPSERT_V2).contains(&op) {
1372 return Err(PayloadError::UnknownOperation(op));
1373 }
1374 decode_mutation(op, bytes)
1375}
1376
1377#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1379pub enum TransactionDecision {
1380 Committed,
1382 Undecided,
1384 Mismatch,
1386}
1387pub fn transaction_decision(
1389 prepared: TransactionBinding,
1390 selected: Option<TransactionBinding>,
1391) -> TransactionDecision {
1392 match selected {
1393 None if prepared.member_count().is_ok() => TransactionDecision::Undecided,
1394 Some(binding) if prepared.member_count().is_ok() && binding == prepared => {
1395 TransactionDecision::Committed
1396 }
1397 _ => TransactionDecision::Mismatch,
1398 }
1399}