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}