Skip to main content

chopin_pg/
protocol.rs

1//! PostgreSQL v3 Wire Protocol message definitions.
2//!
3//! Reference: https://www.postgresql.org/docs/current/protocol-message-formats.html
4
5/// Frontend (client → server) message types.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum FrontendMessage {
8    /// StartupMessage (no tag byte, identified by length + version).
9    Startup,
10    /// Password response ('p').
11    PasswordMessage,
12    /// SASL initial response ('p').
13    SASLInitialResponse,
14    /// SASL response ('p').
15    SASLResponse,
16    /// Parse ('P') — extended query protocol.
17    Parse,
18    /// Bind ('B').
19    Bind,
20    /// Describe ('D').
21    Describe,
22    /// Execute ('E').
23    Execute,
24    /// Sync ('S').
25    Sync,
26    /// Close ('C').
27    Close,
28    /// Query ('Q') — simple query protocol.
29    Query,
30    /// Terminate ('X').
31    Terminate,
32    /// Flush ('H').
33    Flush,
34    /// CopyData ('d').
35    CopyData,
36    /// CopyDone ('c').
37    CopyDone,
38    /// CopyFail ('f').
39    CopyFail,
40}
41
42/// Backend (server → client) message tag bytes.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44#[repr(u8)]
45pub enum BackendTag {
46    AuthenticationRequest = b'R',
47    ParameterStatus = b'S',
48    BackendKeyData = b'K',
49    ReadyForQuery = b'Z',
50    RowDescription = b'T',
51    DataRow = b'D',
52    CommandComplete = b'C',
53    ErrorResponse = b'E',
54    NoticeResponse = b'N',
55    ParseComplete = b'1',
56    BindComplete = b'2',
57    CloseComplete = b'3',
58    NoData = b'n',
59    ParameterDescription = b't',
60    EmptyQueryResponse = b'I',
61    NotificationResponse = b'A',
62    CopyInResponse = b'G',
63    CopyOutResponse = b'H',
64    CopyDone = b'c',
65    CopyData = b'd',
66    NegotiateProtocolVersion = b'v',
67    Unknown = 0,
68}
69
70impl From<u8> for BackendTag {
71    fn from(b: u8) -> Self {
72        match b {
73            b'R' => BackendTag::AuthenticationRequest,
74            b'S' => BackendTag::ParameterStatus,
75            b'K' => BackendTag::BackendKeyData,
76            b'Z' => BackendTag::ReadyForQuery,
77            b'T' => BackendTag::RowDescription,
78            b'D' => BackendTag::DataRow,
79            b'C' => BackendTag::CommandComplete,
80            b'E' => BackendTag::ErrorResponse,
81            b'N' => BackendTag::NoticeResponse,
82            b'1' => BackendTag::ParseComplete,
83            b'2' => BackendTag::BindComplete,
84            b'3' => BackendTag::CloseComplete,
85            b'n' => BackendTag::NoData,
86            b't' => BackendTag::ParameterDescription,
87            b'I' => BackendTag::EmptyQueryResponse,
88            b'A' => BackendTag::NotificationResponse,
89            b'G' => BackendTag::CopyInResponse,
90            b'H' => BackendTag::CopyOutResponse,
91            b'c' => BackendTag::CopyDone,
92            b'd' => BackendTag::CopyData,
93            b'v' => BackendTag::NegotiateProtocolVersion,
94            _ => BackendTag::Unknown,
95        }
96    }
97}
98
99/// Authentication sub-types from AuthenticationRequest messages.
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum AuthType {
102    Ok = 0,
103    CleartextPassword = 3,
104    MD5Password = 5,
105    SASLInit = 10,
106    SASLContinue = 11,
107    SASLFinal = 12,
108}
109
110impl AuthType {
111    pub fn from_i32(v: i32) -> Option<Self> {
112        match v {
113            0 => Some(AuthType::Ok),
114            3 => Some(AuthType::CleartextPassword),
115            5 => Some(AuthType::MD5Password),
116            10 => Some(AuthType::SASLInit),
117            11 => Some(AuthType::SASLContinue),
118            12 => Some(AuthType::SASLFinal),
119            _ => None,
120        }
121    }
122}
123
124/// Transaction status indicator from ReadyForQuery.
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub enum TransactionStatus {
127    /// 'I' — Idle, not in a transaction.
128    Idle,
129    /// 'T' — In a transaction block.
130    InTransaction,
131    /// 'E' — In a failed transaction block.
132    Failed,
133}
134
135impl From<u8> for TransactionStatus {
136    fn from(b: u8) -> Self {
137        match b {
138            b'T' => TransactionStatus::InTransaction,
139            b'E' => TransactionStatus::Failed,
140            _ => TransactionStatus::Idle,
141        }
142    }
143}
144
145/// Describe target: Statement or Portal.
146#[derive(Debug, Clone, Copy)]
147pub enum DescribeTarget {
148    Statement,
149    Portal,
150}
151
152/// Close target: Statement or Portal.
153#[derive(Debug, Clone, Copy)]
154pub enum CloseTarget {
155    Statement,
156    Portal,
157}
158
159/// Column format codes.
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum FormatCode {
162    Text = 0,
163    Binary = 1,
164}
165
166impl From<i16> for FormatCode {
167    fn from(v: i16) -> Self {
168        if v == 1 {
169            FormatCode::Binary
170        } else {
171            FormatCode::Text
172        }
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    // ─── BackendTag::from(u8) ──────────────────────────────────────────────────
181
182    #[test]
183    fn test_backend_tag_auth() {
184        assert_eq!(BackendTag::from(b'R'), BackendTag::AuthenticationRequest);
185    }
186
187    #[test]
188    fn test_backend_tag_parameter_status() {
189        assert_eq!(BackendTag::from(b'S'), BackendTag::ParameterStatus);
190    }
191
192    #[test]
193    fn test_backend_tag_backend_key_data() {
194        assert_eq!(BackendTag::from(b'K'), BackendTag::BackendKeyData);
195    }
196
197    #[test]
198    fn test_backend_tag_ready_for_query() {
199        assert_eq!(BackendTag::from(b'Z'), BackendTag::ReadyForQuery);
200    }
201
202    #[test]
203    fn test_backend_tag_row_description() {
204        assert_eq!(BackendTag::from(b'T'), BackendTag::RowDescription);
205    }
206
207    #[test]
208    fn test_backend_tag_data_row() {
209        assert_eq!(BackendTag::from(b'D'), BackendTag::DataRow);
210    }
211
212    #[test]
213    fn test_backend_tag_command_complete() {
214        assert_eq!(BackendTag::from(b'C'), BackendTag::CommandComplete);
215    }
216
217    #[test]
218    fn test_backend_tag_error_response() {
219        assert_eq!(BackendTag::from(b'E'), BackendTag::ErrorResponse);
220    }
221
222    #[test]
223    fn test_backend_tag_notice_response() {
224        assert_eq!(BackendTag::from(b'N'), BackendTag::NoticeResponse);
225    }
226
227    #[test]
228    fn test_backend_tag_parse_complete() {
229        assert_eq!(BackendTag::from(b'1'), BackendTag::ParseComplete);
230    }
231
232    #[test]
233    fn test_backend_tag_bind_complete() {
234        assert_eq!(BackendTag::from(b'2'), BackendTag::BindComplete);
235    }
236
237    #[test]
238    fn test_backend_tag_close_complete() {
239        assert_eq!(BackendTag::from(b'3'), BackendTag::CloseComplete);
240    }
241
242    #[test]
243    fn test_backend_tag_no_data() {
244        assert_eq!(BackendTag::from(b'n'), BackendTag::NoData);
245    }
246
247    #[test]
248    fn test_backend_tag_parameter_description() {
249        assert_eq!(BackendTag::from(b't'), BackendTag::ParameterDescription);
250    }
251
252    #[test]
253    fn test_backend_tag_empty_query() {
254        assert_eq!(BackendTag::from(b'I'), BackendTag::EmptyQueryResponse);
255    }
256
257    #[test]
258    fn test_backend_tag_notification() {
259        assert_eq!(BackendTag::from(b'A'), BackendTag::NotificationResponse);
260    }
261
262    #[test]
263    fn test_backend_tag_copy_in() {
264        assert_eq!(BackendTag::from(b'G'), BackendTag::CopyInResponse);
265    }
266
267    #[test]
268    fn test_backend_tag_copy_out() {
269        assert_eq!(BackendTag::from(b'H'), BackendTag::CopyOutResponse);
270    }
271
272    #[test]
273    fn test_backend_tag_copy_done() {
274        assert_eq!(BackendTag::from(b'c'), BackendTag::CopyDone);
275    }
276
277    #[test]
278    fn test_backend_tag_copy_data_lowercase() {
279        assert_eq!(BackendTag::from(b'd'), BackendTag::CopyData);
280    }
281
282    #[test]
283    fn test_backend_tag_negotiate_protocol() {
284        assert_eq!(BackendTag::from(b'v'), BackendTag::NegotiateProtocolVersion);
285    }
286
287    #[test]
288    fn test_backend_tag_unknown_byte() {
289        assert_eq!(BackendTag::from(0xFF), BackendTag::Unknown);
290    }
291
292    #[test]
293    fn test_backend_tag_unknown_zero() {
294        assert_eq!(BackendTag::from(0x00), BackendTag::Unknown);
295    }
296
297    #[test]
298    fn test_backend_tag_debug_format() {
299        let tag = BackendTag::ReadyForQuery;
300        let s = format!("{:?}", tag);
301        assert_eq!(s, "ReadyForQuery");
302    }
303
304    #[test]
305    fn test_backend_tag_equality() {
306        assert_eq!(BackendTag::from(b'Z'), BackendTag::ReadyForQuery);
307        assert_ne!(BackendTag::from(b'Z'), BackendTag::ErrorResponse);
308    }
309
310    #[test]
311    fn test_backend_tag_repr_values() {
312        // Verify the repr(u8) byte values match the protocol spec
313        assert_eq!(BackendTag::AuthenticationRequest as u8, b'R');
314        assert_eq!(BackendTag::ReadyForQuery as u8, b'Z');
315        assert_eq!(BackendTag::ErrorResponse as u8, b'E');
316        assert_eq!(BackendTag::DataRow as u8, b'D');
317        assert_eq!(BackendTag::CommandComplete as u8, b'C');
318    }
319
320    // ─── TransactionStatus::from(u8) ─────────────────────────────────────────
321
322    #[test]
323    fn test_tx_status_idle_from_i() {
324        assert_eq!(TransactionStatus::from(b'I'), TransactionStatus::Idle);
325    }
326
327    #[test]
328    fn test_tx_status_in_transaction() {
329        assert_eq!(
330            TransactionStatus::from(b'T'),
331            TransactionStatus::InTransaction
332        );
333    }
334
335    #[test]
336    fn test_tx_status_failed() {
337        assert_eq!(TransactionStatus::from(b'E'), TransactionStatus::Failed);
338    }
339
340    #[test]
341    fn test_tx_status_unknown_defaults_idle() {
342        // Any unrecognized byte → Idle (safe default)
343        assert_eq!(TransactionStatus::from(0xFF), TransactionStatus::Idle);
344        assert_eq!(TransactionStatus::from(b'X'), TransactionStatus::Idle);
345    }
346
347    #[test]
348    fn test_tx_status_debug() {
349        assert_eq!(format!("{:?}", TransactionStatus::Idle), "Idle");
350        assert_eq!(
351            format!("{:?}", TransactionStatus::InTransaction),
352            "InTransaction"
353        );
354        assert_eq!(format!("{:?}", TransactionStatus::Failed), "Failed");
355    }
356
357    #[test]
358    fn test_tx_status_eq() {
359        assert_eq!(TransactionStatus::Idle, TransactionStatus::Idle);
360        assert_ne!(TransactionStatus::Idle, TransactionStatus::InTransaction);
361    }
362
363    // ─── AuthType::from_i32 ──────────────────────────────────────────────────
364
365    #[test]
366    fn test_auth_type_ok() {
367        assert_eq!(AuthType::from_i32(0), Some(AuthType::Ok));
368    }
369
370    #[test]
371    fn test_auth_type_cleartext() {
372        assert_eq!(AuthType::from_i32(3), Some(AuthType::CleartextPassword));
373    }
374
375    #[test]
376    fn test_auth_type_md5() {
377        assert_eq!(AuthType::from_i32(5), Some(AuthType::MD5Password));
378    }
379
380    #[test]
381    fn test_auth_type_sasl_init() {
382        assert_eq!(AuthType::from_i32(10), Some(AuthType::SASLInit));
383    }
384
385    #[test]
386    fn test_auth_type_sasl_continue() {
387        assert_eq!(AuthType::from_i32(11), Some(AuthType::SASLContinue));
388    }
389
390    #[test]
391    fn test_auth_type_sasl_final() {
392        assert_eq!(AuthType::from_i32(12), Some(AuthType::SASLFinal));
393    }
394
395    #[test]
396    fn test_auth_type_unknown_returns_none() {
397        assert_eq!(AuthType::from_i32(1), None);
398        assert_eq!(AuthType::from_i32(99), None);
399        assert_eq!(AuthType::from_i32(-1), None);
400    }
401
402    // ─── FormatCode::from(i16) ───────────────────────────────────────────────
403
404    #[test]
405    fn test_format_code_text_from_zero() {
406        assert_eq!(FormatCode::from(0i16), FormatCode::Text);
407    }
408
409    #[test]
410    fn test_format_code_binary_from_one() {
411        assert_eq!(FormatCode::from(1i16), FormatCode::Binary);
412    }
413
414    #[test]
415    fn test_format_code_unknown_defaults_text() {
416        // Anything other than 1 → Text (safe default)
417        assert_eq!(FormatCode::from(2i16), FormatCode::Text);
418        assert_eq!(FormatCode::from(-1i16), FormatCode::Text);
419        assert_eq!(FormatCode::from(99i16), FormatCode::Text);
420    }
421
422    #[test]
423    fn test_format_code_values() {
424        assert_eq!(FormatCode::Text as i16, 0);
425        assert_eq!(FormatCode::Binary as i16, 1);
426    }
427
428    #[test]
429    fn test_format_code_eq() {
430        assert_eq!(FormatCode::Text, FormatCode::Text);
431        assert_ne!(FormatCode::Text, FormatCode::Binary);
432    }
433
434    // ─── Roundtrip: from → tag byte → from ───────────────────────────────────
435
436    #[test]
437    fn test_all_known_backend_tags_roundtrip() {
438        let pairs: &[(u8, BackendTag)] = &[
439            (b'R', BackendTag::AuthenticationRequest),
440            (b'S', BackendTag::ParameterStatus),
441            (b'K', BackendTag::BackendKeyData),
442            (b'Z', BackendTag::ReadyForQuery),
443            (b'T', BackendTag::RowDescription),
444            (b'D', BackendTag::DataRow),
445            (b'C', BackendTag::CommandComplete),
446            (b'E', BackendTag::ErrorResponse),
447            (b'N', BackendTag::NoticeResponse),
448            (b'1', BackendTag::ParseComplete),
449            (b'2', BackendTag::BindComplete),
450            (b'3', BackendTag::CloseComplete),
451            (b'n', BackendTag::NoData),
452            (b't', BackendTag::ParameterDescription),
453            (b'I', BackendTag::EmptyQueryResponse),
454            (b'A', BackendTag::NotificationResponse),
455            (b'G', BackendTag::CopyInResponse),
456            (b'H', BackendTag::CopyOutResponse),
457            (b'c', BackendTag::CopyDone),
458            (b'd', BackendTag::CopyData),
459            (b'v', BackendTag::NegotiateProtocolVersion),
460        ];
461        for &(byte, ref expected) in pairs {
462            let got = BackendTag::from(byte);
463            assert_eq!(
464                &got, expected,
465                "byte {:#04x} should map to {:?}",
466                byte, expected
467            );
468        }
469    }
470}