Skip to main content

zero_postgres/conversion/
ref_row.rs

1//! Zero-copy row decoding for fixed-size types.
2//!
3//! This module provides traits and types for zero-copy decoding of database rows
4//! where all fields have fixed wire sizes. This is useful for high-performance
5//! scenarios where avoiding allocations is critical.
6//!
7//! # PostgreSQL Wire Format
8//!
9//! PostgreSQL's binary protocol includes a 4-byte length prefix before each
10//! column value. To enable zero-copy struct casting, use [`LengthPrefixed<T>`]
11//! wrapper which accounts for this prefix in its layout.
12//!
13//! # Requirements
14//!
15//! - All struct fields must implement `FixedWireSize`
16//! - All columns must be `NOT NULL` (no `Option<T>` support)
17//! - Struct must use `#[repr(C, packed)]` for predictable layout
18//! - Fields must use `LengthPrefixed<T>` with big-endian types
19//!
20//! # Compile-time Safety
21//!
22//! The derive macro enforces that all fields implement `FixedWireSize` at compile time.
23//! Variable-length types like `String` or `Vec<T>` will cause a compilation error.
24
25use zerocopy::{FromBytes, Immutable, KnownLayout};
26
27use crate::Result;
28use crate::protocol::backend::query::{DataRow, FieldDescription};
29
30// Re-export big-endian types for convenience
31pub use zerocopy::big_endian::{
32    I16 as I16BE, I32 as I32BE, I64 as I64BE, U16 as U16BE, U32 as U32BE, U64 as U64BE,
33};
34
35/// Marker trait for types with a fixed wire size in PostgreSQL binary protocol.
36///
37/// This trait is only implemented for types that have a guaranteed fixed size
38/// on the wire. For use with `RefFromRow`, wrap types in [`LengthPrefixed<T>`]
39/// to account for PostgreSQL's length prefix.
40pub trait FixedWireSize {
41    /// The fixed size in bytes on the wire.
42    const WIRE_SIZE: usize;
43}
44
45// Single-byte types are endian-agnostic
46impl FixedWireSize for i8 {
47    const WIRE_SIZE: usize = 1;
48}
49impl FixedWireSize for u8 {
50    const WIRE_SIZE: usize = 1;
51}
52
53// Big-endian integer types (PostgreSQL wire format / network byte order)
54impl FixedWireSize for zerocopy::big_endian::I16 {
55    const WIRE_SIZE: usize = 2;
56}
57impl FixedWireSize for zerocopy::big_endian::U16 {
58    const WIRE_SIZE: usize = 2;
59}
60impl FixedWireSize for zerocopy::big_endian::I32 {
61    const WIRE_SIZE: usize = 4;
62}
63impl FixedWireSize for zerocopy::big_endian::U32 {
64    const WIRE_SIZE: usize = 4;
65}
66impl FixedWireSize for zerocopy::big_endian::I64 {
67    const WIRE_SIZE: usize = 8;
68}
69impl FixedWireSize for zerocopy::big_endian::U64 {
70    const WIRE_SIZE: usize = 8;
71}
72
73/// A length-prefixed value matching PostgreSQL's binary wire format.
74///
75/// PostgreSQL's binary protocol prefixes each column value with a 4-byte
76/// signed integer length (or -1 for NULL). This wrapper type includes that
77/// prefix, allowing zero-copy struct casting from raw row data.
78///
79/// # Layout
80///
81/// ```text
82/// [length: i32 BE][value: T]
83/// ```
84///
85/// Total size: 4 + size_of::<T>() bytes
86///
87/// # Example
88///
89/// ```ignore
90/// use zero_postgres::conversion::ref_row::{LengthPrefixed, I64BE};
91///
92/// // Wire format: [0x00 0x00 0x00 0x08][8 bytes of i64]
93/// let data: &[u8] = &[0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 42];
94/// let prefixed: &LengthPrefixed<I64BE> = zerocopy::FromBytes::ref_from_bytes(data).unwrap();
95/// assert_eq!(prefixed.get(), 42);
96/// ```
97#[derive(Debug, Clone, Copy, FromBytes, KnownLayout, Immutable)]
98#[repr(C, packed)]
99pub struct LengthPrefixed<T> {
100    /// Length prefix (big-endian i32). Should equal size_of::<T>() for valid data.
101    len: zerocopy::big_endian::I32,
102    /// The actual value.
103    value: T,
104}
105
106impl<T: FixedWireSize> FixedWireSize for LengthPrefixed<T> {
107    /// Wire size = 4 bytes (length prefix) + inner type's wire size
108    const WIRE_SIZE: usize = 4 + T::WIRE_SIZE;
109}
110
111impl<T: Copy> LengthPrefixed<T> {
112    /// Get the length prefix value.
113    #[inline]
114    pub fn len(&self) -> i32 {
115        self.len.get()
116    }
117
118    /// Check if the length is zero.
119    #[inline]
120    pub fn is_empty(&self) -> bool {
121        self.len.get() == 0
122    }
123
124    /// Check if the length indicates NULL (-1).
125    #[inline]
126    pub fn is_null(&self) -> bool {
127        self.len.get() == -1
128    }
129
130    /// Get the inner value.
131    ///
132    /// Note: This copies the value because packed structs cannot safely
133    /// return references to potentially unaligned fields.
134    #[inline]
135    pub fn get(&self) -> T {
136        self.value
137    }
138}
139
140/// Trait for zero-copy decoding of a row into a fixed-size struct.
141///
142/// Unlike `FromRow`, this trait returns a reference directly into the buffer
143/// without any copying or allocation. This requires:
144///
145/// 1. All fields are `LengthPrefixed<T>` where `T: FixedWireSize`
146/// 2. No NULL values (columns must be `NOT NULL`)
147/// 3. Struct has `#[repr(C, packed)]` layout
148///
149/// The derive macro generates zerocopy trait implementations automatically.
150///
151/// # Compile-fail tests
152///
153/// Missing `#[repr(C, packed)]`:
154/// ```compile_fail
155/// use zero_postgres::conversion::ref_row::{LengthPrefixed, I32BE};
156/// use zero_postgres_derive::RefFromRow;
157///
158/// #[derive(RefFromRow)]
159/// struct Invalid {
160///     value: LengthPrefixed<I32BE>,
161/// }
162/// ```
163///
164/// Native integer types (must use big-endian wrappers):
165/// ```compile_fail
166/// use zero_postgres::conversion::ref_row::LengthPrefixed;
167/// use zero_postgres_derive::RefFromRow;
168///
169/// #[derive(RefFromRow)]
170/// #[repr(C, packed)]
171/// struct Invalid {
172///     value: LengthPrefixed<i64>,
173/// }
174/// ```
175///
176/// `String` fields are not allowed:
177/// ```compile_fail
178/// use zero_postgres_derive::RefFromRow;
179///
180/// #[derive(RefFromRow)]
181/// #[repr(C, packed)]
182/// struct Invalid {
183///     name: String,
184/// }
185/// ```
186///
187/// `Vec` fields are not allowed:
188/// ```compile_fail
189/// use zero_postgres_derive::RefFromRow;
190///
191/// #[derive(RefFromRow)]
192/// #[repr(C, packed)]
193/// struct Invalid {
194///     data: Vec<u8>,
195/// }
196/// ```
197pub trait RefFromRow<'a>: Sized {
198    /// Decode a row as a zero-copy reference from binary format.
199    ///
200    /// # Errors
201    ///
202    /// Returns an error if:
203    /// - The row data size doesn't match the struct size
204    /// - Any column is NULL (indicated by length = -1)
205    fn ref_from_row_binary(cols: &[FieldDescription], row: DataRow<'a>) -> Result<&'a Self>;
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn length_prefixed_i32() {
214        // Wire format: length=4 (BE), value=42 (BE)
215        let data: &[u8] = &[0, 0, 0, 4, 0, 0, 0, 42];
216        let prefixed: &LengthPrefixed<I32BE> = zerocopy::FromBytes::ref_from_bytes(data).unwrap();
217
218        assert_eq!(prefixed.len(), 4);
219        assert!(!prefixed.is_null());
220        assert_eq!(prefixed.get().get(), 42);
221    }
222
223    #[test]
224    fn length_prefixed_i64() {
225        // Wire format: length=8 (BE), value=12345678901234 (BE)
226        let value: i64 = 12345678901234;
227        let mut data = [0u8; 12];
228        data[0..4].copy_from_slice(&8_i32.to_be_bytes());
229        data[4..12].copy_from_slice(&value.to_be_bytes());
230
231        let prefixed: &LengthPrefixed<I64BE> = zerocopy::FromBytes::ref_from_bytes(&data).unwrap();
232
233        assert_eq!(prefixed.len(), 8);
234        assert_eq!(prefixed.get().get(), value);
235    }
236
237    #[test]
238    fn wire_size() {
239        assert_eq!(<LengthPrefixed<I32BE> as FixedWireSize>::WIRE_SIZE, 8);
240        assert_eq!(<LengthPrefixed<I64BE> as FixedWireSize>::WIRE_SIZE, 12);
241        assert_eq!(<LengthPrefixed<I16BE> as FixedWireSize>::WIRE_SIZE, 6);
242    }
243
244    #[test]
245    fn contiguous_struct() {
246        // Simulate a row with two columns: INT4 (42) and INT8 (12345)
247        // Wire format: [len=4][val=42][len=8][val=12345]
248        let mut data = [0u8; 20]; // 8 + 12 = 20 bytes
249        // First column: INT4
250        data[0..4].copy_from_slice(&4_i32.to_be_bytes());
251        data[4..8].copy_from_slice(&42_i32.to_be_bytes());
252        // Second column: INT8
253        data[8..12].copy_from_slice(&8_i32.to_be_bytes());
254        data[12..20].copy_from_slice(&12345_i64.to_be_bytes());
255
256        #[derive(FromBytes, KnownLayout, Immutable)]
257        #[repr(C, packed)]
258        struct TestRow {
259            col1: LengthPrefixed<I32BE>,
260            col2: LengthPrefixed<I64BE>,
261        }
262
263        let row: &TestRow = zerocopy::FromBytes::ref_from_bytes(&data).unwrap();
264        assert_eq!(row.col1.get().get(), 42);
265        assert_eq!(row.col2.get().get(), 12345);
266    }
267}