Skip to main content

pg_wired/protocol/
types.rs

1/// PostgreSQL OID type.
2pub type Oid = u32;
3
4/// Wire format codes.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6#[repr(i16)]
7#[non_exhaustive]
8pub enum FormatCode {
9    Text = 0,
10    Binary = 1,
11}
12
13/// Frontend (client → server) messages.
14#[derive(Debug)]
15#[non_exhaustive]
16pub enum FrontendMsg<'a> {
17    /// Parse: prepare a statement.
18    /// name (empty = unnamed), sql, param OIDs
19    Parse {
20        name: &'a [u8],
21        sql: &'a [u8],
22        param_oids: &'a [Oid],
23    },
24    /// Bind: bind parameters to a prepared statement.
25    /// portal (empty = unnamed), statement name, param formats, param values, result formats
26    Bind {
27        portal: &'a [u8],
28        statement: &'a [u8],
29        param_formats: &'a [FormatCode],
30        params: &'a [Option<&'a [u8]>],
31        result_formats: &'a [FormatCode],
32    },
33    /// Execute: execute a bound portal.
34    Execute { portal: &'a [u8], max_rows: i32 },
35    /// Sync: end of pipeline, triggers response flush.
36    Sync,
37    /// Query: simple query protocol (text only).
38    Query(&'a [u8]),
39    /// Describe: request description of a statement or portal.
40    Describe {
41        kind: u8, // b'S' = statement, b'P' = portal
42        name: &'a [u8],
43    },
44    /// Close: close a prepared statement or portal.
45    Close {
46        kind: u8, // b'S' = statement, b'P' = portal
47        name: &'a [u8],
48    },
49    /// Flush: request server to flush output.
50    Flush,
51    /// SASL initial response.
52    SASLInitialResponse { mechanism: &'a [u8], data: &'a [u8] },
53    /// SASL response (continuation).
54    SASLResponse(&'a [u8]),
55    /// CopyData: a chunk of COPY data sent to server.
56    CopyData(&'a [u8]),
57    /// CopyDone: signal that COPY data is complete.
58    CopyDone,
59    /// CopyFail: abort COPY with an error message.
60    CopyFail(&'a [u8]),
61    /// Terminate: close connection.
62    Terminate,
63}
64
65/// Backend (server → client) messages.
66#[derive(Debug)]
67#[non_exhaustive]
68pub enum BackendMsg {
69    AuthenticationOk,
70    AuthenticationCleartextPassword,
71    AuthenticationMd5Password {
72        salt: [u8; 4],
73    },
74    AuthenticationSASL {
75        mechanisms: Vec<String>,
76    },
77    AuthenticationSASLContinue {
78        data: Vec<u8>,
79    },
80    AuthenticationSASLFinal {
81        data: Vec<u8>,
82    },
83    ParameterStatus {
84        name: String,
85        value: String,
86    },
87    BackendKeyData {
88        pid: i32,
89        secret: i32,
90    },
91    ReadyForQuery {
92        status: u8,
93    },
94    ParseComplete,
95    BindComplete,
96    CloseComplete,
97    NoData,
98    CommandComplete {
99        tag: String,
100    },
101    DataRow(RawRow),
102    RowDescription {
103        fields: Vec<FieldDescription>,
104    },
105    ErrorResponse {
106        fields: PgError,
107    },
108    NoticeResponse {
109        fields: PgError,
110    },
111    EmptyQueryResponse,
112    /// ParameterDescription: param type OIDs from a Describe Statement.
113    ParameterDescription {
114        type_oids: Vec<Oid>,
115    },
116    /// NotificationResponse: async notification from LISTEN/NOTIFY.
117    NotificationResponse {
118        pid: i32,
119        channel: String,
120        payload: String,
121    },
122    /// PortalSuspended: Execute completed with row limit, portal still open.
123    PortalSuspended,
124    /// CopyInResponse: server is ready to receive COPY data.
125    CopyInResponse {
126        format: u8, // 0=text, 1=binary
127        column_formats: Vec<i16>,
128    },
129    /// CopyOutResponse: server is about to send COPY data.
130    CopyOutResponse {
131        format: u8,
132        column_formats: Vec<i16>,
133    },
134    /// CopyData: a chunk of COPY data (in either direction).
135    CopyData {
136        data: Vec<u8>,
137    },
138    /// CopyDone: COPY stream completed.
139    CopyDone,
140}
141
142/// A parsed wire-protocol DataRow. Cell `(offset, length)` pairs (length
143/// `-1` = NULL) are stored inline in the row up to [`CELL_INLINE_CAP`]
144/// columns, falling back to a `Box<[(u32, i32)]>` for wider rows. Inline
145/// storage avoids the per-row Arc bump/decrement that a shared `Bytes`
146/// would impose. Cell values point into `body`.
147pub const CELL_INLINE_CAP: usize = 12;
148
149#[derive(Debug, Clone)]
150pub struct RawRow {
151    pub(crate) body: bytes::Bytes,
152    cells: Cells,
153}
154
155impl RawRow {
156    /// Raw wire-protocol body the cell offsets index into. Exposed for
157    /// downstream typed-row decoders (e.g., `resolute::Row`); offset/length
158    /// semantics are an internal contract, treat as opaque.
159    #[doc(hidden)]
160    pub fn body(&self) -> &bytes::Bytes {
161        &self.body
162    }
163}
164
165#[derive(Debug, Clone)]
166enum Cells {
167    Inline {
168        data: [(u32, i32); CELL_INLINE_CAP],
169        len: u8,
170    },
171    Heap(Box<[(u32, i32)]>),
172}
173
174impl RawRow {
175    /// Empty row (zero columns).
176    pub fn empty() -> Self {
177        Self {
178            body: bytes::Bytes::new(),
179            cells: Cells::Inline {
180                data: [(0, 0); CELL_INLINE_CAP],
181                len: 0,
182            },
183        }
184    }
185
186    /// Construct directly from an inline cell buffer. Caller must ensure
187    /// `len <= CELL_INLINE_CAP` and that the first `len` entries in `data`
188    /// are valid (offset+length lies within `body` for non-NULL cells).
189    #[inline]
190    pub fn from_inline_unchecked(
191        body: bytes::Bytes,
192        data: [(u32, i32); CELL_INLINE_CAP],
193        len: u8,
194    ) -> Self {
195        debug_assert!(len as usize <= CELL_INLINE_CAP);
196        Self {
197            body,
198            cells: Cells::Inline { data, len },
199        }
200    }
201
202    /// Construct from a body and a slice of `(offset, length)` pairs.
203    /// Caller must guarantee every non-NULL entry `(offset, length)` lies
204    /// within `body`.
205    pub fn from_entries(body: bytes::Bytes, entries: &[(u32, i32)]) -> Self {
206        let cells = if entries.len() <= CELL_INLINE_CAP {
207            let mut data = [(0u32, 0i32); CELL_INLINE_CAP];
208            data[..entries.len()].copy_from_slice(entries);
209            Cells::Inline {
210                data,
211                len: entries.len() as u8,
212            }
213        } else {
214            Cells::Heap(entries.into())
215        };
216        Self { body, cells }
217    }
218
219    /// Construct a single-cell row pointing at the entirety of `body`.
220    /// Used for COPY data chunks.
221    pub fn from_full_body(body: bytes::Bytes) -> Self {
222        let len = body.len() as i32;
223        let mut data = [(0u32, 0i32); CELL_INLINE_CAP];
224        data[0] = (0, len);
225        Self {
226            body,
227            cells: Cells::Inline { data, len: 1 },
228        }
229    }
230
231    fn entries(&self) -> &[(u32, i32)] {
232        match &self.cells {
233            Cells::Inline { data, len } => &data[..*len as usize],
234            Cells::Heap(b) => b,
235        }
236    }
237
238    pub fn len(&self) -> usize {
239        self.entries().len()
240    }
241
242    pub fn is_empty(&self) -> bool {
243        self.entries().is_empty()
244    }
245
246    /// Get cell `idx` as a `&[u8]`. Returns `None` for NULL **or** out-of-range.
247    /// Use [`Self::try_cell`] when you need to distinguish the two.
248    pub fn cell(&self, idx: usize) -> Option<&[u8]> {
249        let (off, len) = *self.entries().get(idx)?;
250        if len < 0 {
251            return None;
252        }
253        let start = off as usize;
254        let end = start + len as usize;
255        Some(&self.body[start..end])
256    }
257
258    /// Outer `None` = out-of-range; inner `None` = SQL NULL; `Some(Some(_))` = bytes.
259    pub fn try_cell(&self, idx: usize) -> Option<Option<&[u8]>> {
260        let (off, len) = *self.entries().get(idx)?;
261        if len < 0 {
262            return Some(None);
263        }
264        let start = off as usize;
265        let end = start + len as usize;
266        Some(Some(&self.body[start..end]))
267    }
268
269    /// Iterate over cells; `None` items are SQL NULLs.
270    pub fn iter(&self) -> impl Iterator<Item = Option<&[u8]>> + '_ {
271        let body = self.body.as_ref();
272        self.entries().iter().map(move |&(off, len)| {
273            if len < 0 {
274                None
275            } else {
276                let start = off as usize;
277                let end = start + len as usize;
278                Some(&body[start..end])
279            }
280        })
281    }
282}
283
284#[derive(Debug, Clone)]
285#[non_exhaustive]
286pub struct FieldDescription {
287    pub name: String,
288    pub table_oid: Oid,
289    pub column_id: i16,
290    pub type_oid: Oid,
291    pub type_size: i16,
292    pub type_modifier: i32,
293    pub format: FormatCode,
294}
295
296impl Default for FieldDescription {
297    fn default() -> Self {
298        Self {
299            name: String::new(),
300            table_oid: 0,
301            column_id: 0,
302            type_oid: 0,
303            type_size: -1,
304            type_modifier: -1,
305            format: FormatCode::Binary,
306        }
307    }
308}
309
310#[derive(Debug, Clone, Default)]
311#[non_exhaustive]
312pub struct PgError {
313    pub severity: String,
314    pub code: String,
315    pub message: String,
316    pub detail: Option<String>,
317    pub hint: Option<String>,
318    pub position: Option<String>,
319}