Skip to main content

qail_pg/protocol/
encoder.rs

1//! PostgreSQL Encoder (Visitor Pattern)
2//!
3//! Compiles Qail AST into PostgreSQL wire protocol bytes.
4//! This is pure, synchronous computation - no I/O, no async.
5//!
6//! # Architecture
7//!
8//! Layer 2 of the QAIL architecture:
9//! - Input: Qail (AST)
10//! - Output: BytesMut (ready to send over the wire)
11//!
12//! The async I/O layer (Layer 3) consumes these bytes.
13
14use super::EncodeError;
15use bytes::BytesMut;
16
17/// Takes a Qail and produces wire protocol bytes.
18/// This is the "Visitor" in the visitor pattern.
19pub struct PgEncoder;
20
21impl PgEncoder {
22    /// Wire format code for text columns.
23    pub const FORMAT_TEXT: i16 = 0;
24    /// Wire format code for binary columns.
25    pub const FORMAT_BINARY: i16 = 1;
26
27    #[inline(always)]
28    fn param_format_wire_len(param_format: i16) -> usize {
29        if param_format == Self::FORMAT_TEXT {
30            2 // parameter format count = 0 (server default text)
31        } else {
32            4 // parameter format count = 1 + one format code for all parameters
33        }
34    }
35
36    #[inline(always)]
37    fn encode_param_formats_vec(content: &mut Vec<u8>, param_format: i16) {
38        if param_format == Self::FORMAT_TEXT {
39            content.extend_from_slice(&0i16.to_be_bytes());
40        } else {
41            content.extend_from_slice(&1i16.to_be_bytes());
42            content.extend_from_slice(&param_format.to_be_bytes());
43        }
44    }
45
46    #[inline(always)]
47    fn encode_param_formats_bytesmut(buf: &mut BytesMut, param_format: i16) {
48        if param_format == Self::FORMAT_TEXT {
49            buf.extend_from_slice(&0i16.to_be_bytes());
50        } else {
51            buf.extend_from_slice(&1i16.to_be_bytes());
52            buf.extend_from_slice(&param_format.to_be_bytes());
53        }
54    }
55
56    #[inline(always)]
57    fn result_format_wire_len(result_format: i16) -> usize {
58        if result_format == Self::FORMAT_TEXT {
59            2 // result format count = 0
60        } else {
61            4 // result format count = 1 + one format code
62        }
63    }
64
65    #[inline(always)]
66    fn encode_result_formats_vec(content: &mut Vec<u8>, result_format: i16) {
67        if result_format == Self::FORMAT_TEXT {
68            content.extend_from_slice(&0i16.to_be_bytes());
69        } else {
70            content.extend_from_slice(&1i16.to_be_bytes());
71            content.extend_from_slice(&result_format.to_be_bytes());
72        }
73    }
74
75    #[inline(always)]
76    fn encode_result_formats_bytesmut(buf: &mut BytesMut, result_format: i16) {
77        if result_format == Self::FORMAT_TEXT {
78            buf.extend_from_slice(&0i16.to_be_bytes());
79        } else {
80            buf.extend_from_slice(&1i16.to_be_bytes());
81            buf.extend_from_slice(&result_format.to_be_bytes());
82        }
83    }
84
85    #[inline(always)]
86    fn content_len_to_wire_len(content_len: usize) -> Result<i32, EncodeError> {
87        let total = content_len
88            .checked_add(4)
89            .ok_or(EncodeError::MessageTooLarge(usize::MAX))?;
90        i32::try_from(total).map_err(|_| EncodeError::MessageTooLarge(total))
91    }
92
93    #[inline(always)]
94    fn usize_to_i16(n: usize) -> Result<i16, EncodeError> {
95        i16::try_from(n).map_err(|_| EncodeError::TooManyParameters(n))
96    }
97
98    #[inline(always)]
99    fn usize_to_i32(n: usize) -> Result<i32, EncodeError> {
100        i32::try_from(n).map_err(|_| EncodeError::MessageTooLarge(n))
101    }
102
103    #[inline(always)]
104    fn has_nul(s: &str) -> bool {
105        s.as_bytes().contains(&0)
106    }
107
108    /// Fallible simple-query encoder.
109    pub fn try_encode_query_string(sql: &str) -> Result<BytesMut, EncodeError> {
110        if Self::has_nul(sql) {
111            return Err(EncodeError::NullByte);
112        }
113
114        let mut buf = BytesMut::new();
115        let content_len = sql.len() + 1; // +1 for null terminator
116        let total_len = Self::content_len_to_wire_len(content_len)?;
117
118        buf.extend_from_slice(b"Q");
119        buf.extend_from_slice(&total_len.to_be_bytes());
120        buf.extend_from_slice(sql.as_bytes());
121        buf.extend_from_slice(&[0]);
122        Ok(buf)
123    }
124
125    /// Encode a Terminate message to close the connection.
126    pub fn encode_terminate() -> BytesMut {
127        let mut buf = BytesMut::new();
128        buf.extend_from_slice(&[b'X', 0, 0, 0, 4]);
129        buf
130    }
131
132    /// Encode a Sync message (end of pipeline in extended query protocol).
133    pub fn encode_sync() -> BytesMut {
134        let mut buf = BytesMut::new();
135        buf.extend_from_slice(&[b'S', 0, 0, 0, 4]);
136        buf
137    }
138
139    // ==================== Extended Query Protocol ====================
140
141    /// Fallible Parse message encoder.
142    pub fn try_encode_parse(
143        name: &str,
144        sql: &str,
145        param_types: &[u32],
146    ) -> Result<BytesMut, EncodeError> {
147        if Self::has_nul(name) || Self::has_nul(sql) {
148            return Err(EncodeError::NullByte);
149        }
150        if param_types.len() > i16::MAX as usize {
151            return Err(EncodeError::TooManyParameters(param_types.len()));
152        }
153
154        let mut buf = BytesMut::new();
155        buf.extend_from_slice(b"P");
156
157        let mut content = Vec::new();
158        content.extend_from_slice(name.as_bytes());
159        content.push(0);
160        content.extend_from_slice(sql.as_bytes());
161        content.push(0);
162        let param_count = Self::usize_to_i16(param_types.len())?;
163        content.extend_from_slice(&param_count.to_be_bytes());
164        for &oid in param_types {
165            content.extend_from_slice(&oid.to_be_bytes());
166        }
167
168        let len = Self::content_len_to_wire_len(content.len())?;
169        buf.extend_from_slice(&len.to_be_bytes());
170        buf.extend_from_slice(&content);
171        Ok(buf)
172    }
173
174    /// Encode a Bind message (bind parameters to a prepared statement).
175    /// Wire format:
176    /// - 'B' (1 byte) - message type
177    /// - length (4 bytes)
178    /// - portal name (null-terminated)
179    /// - statement name (null-terminated)
180    /// - format code section (2-4 bytes) - default path uses 0 (all text)
181    /// - parameter count (2 bytes)
182    /// - for each parameter: length (4 bytes, -1 for NULL), data
183    /// - result format count + codes
184    ///
185    /// # Arguments
186    ///
187    /// * `portal` — Destination portal name (empty string for unnamed).
188    /// * `statement` — Source prepared statement name (empty string for unnamed).
189    /// * `params` — Parameter values; `None` entries encode as SQL NULL.
190    pub fn encode_bind(
191        portal: &str,
192        statement: &str,
193        params: &[Option<Vec<u8>>],
194    ) -> Result<BytesMut, EncodeError> {
195        Self::encode_bind_with_result_format(portal, statement, params, Self::FORMAT_TEXT)
196    }
197
198    /// Encode a Bind message with explicit result-column format.
199    ///
200    /// `result_format` is PostgreSQL wire format code: `0 = text`, `1 = binary`.
201    /// For `0`, this encodes "result format count = 0" (server default text).
202    /// For non-zero codes, this encodes one explicit result format code.
203    pub fn encode_bind_with_result_format(
204        portal: &str,
205        statement: &str,
206        params: &[Option<Vec<u8>>],
207        result_format: i16,
208    ) -> Result<BytesMut, EncodeError> {
209        Self::encode_bind_with_formats(portal, statement, params, Self::FORMAT_TEXT, result_format)
210    }
211
212    /// Encode a Bind message with explicit parameter and result format codes.
213    ///
214    /// `param_format` / `result_format` are PostgreSQL wire format codes:
215    /// `0 = text`, `1 = binary`.
216    ///
217    /// For `param_format = 0`, this encodes "parameter format count = 0"
218    /// (server default text). For non-zero, this encodes one explicit format
219    /// code applied to all parameters.
220    pub fn encode_bind_with_formats(
221        portal: &str,
222        statement: &str,
223        params: &[Option<Vec<u8>>],
224        param_format: i16,
225        result_format: i16,
226    ) -> Result<BytesMut, EncodeError> {
227        if Self::has_nul(portal) || Self::has_nul(statement) {
228            return Err(EncodeError::NullByte);
229        }
230        if params.len() > i16::MAX as usize {
231            return Err(EncodeError::TooManyParameters(params.len()));
232        }
233
234        let mut buf = BytesMut::new();
235
236        // Message type 'B'
237        buf.extend_from_slice(b"B");
238
239        let mut content = Vec::new();
240
241        // Portal name (null-terminated)
242        content.extend_from_slice(portal.as_bytes());
243        content.push(0);
244
245        // Statement name (null-terminated)
246        content.extend_from_slice(statement.as_bytes());
247        content.push(0);
248
249        // Parameter format codes
250        Self::encode_param_formats_vec(&mut content, param_format);
251
252        // Parameter count
253        let param_count = Self::usize_to_i16(params.len())?;
254        content.extend_from_slice(&param_count.to_be_bytes());
255
256        // Parameters
257        for param in params {
258            match param {
259                None => {
260                    // NULL: length = -1
261                    content.extend_from_slice(&(-1i32).to_be_bytes());
262                }
263                Some(data) => {
264                    let data_len = Self::usize_to_i32(data.len())?;
265                    content.extend_from_slice(&data_len.to_be_bytes());
266                    content.extend_from_slice(data);
267                }
268            }
269        }
270
271        // Result format codes: default text (count=0) or explicit code.
272        Self::encode_result_formats_vec(&mut content, result_format);
273
274        // Length
275        let len = Self::content_len_to_wire_len(content.len())?;
276        buf.extend_from_slice(&len.to_be_bytes());
277        buf.extend_from_slice(&content);
278
279        Ok(buf)
280    }
281
282    /// Fallible Execute message encoder.
283    pub fn try_encode_execute(portal: &str, max_rows: i32) -> Result<BytesMut, EncodeError> {
284        if Self::has_nul(portal) {
285            return Err(EncodeError::NullByte);
286        }
287        if max_rows < 0 {
288            return Err(EncodeError::InvalidMaxRows(max_rows));
289        }
290
291        let mut buf = BytesMut::new();
292        buf.extend_from_slice(b"E");
293
294        let mut content = Vec::new();
295        content.extend_from_slice(portal.as_bytes());
296        content.push(0);
297        content.extend_from_slice(&max_rows.to_be_bytes());
298
299        let len = Self::content_len_to_wire_len(content.len())?;
300        buf.extend_from_slice(&len.to_be_bytes());
301        buf.extend_from_slice(&content);
302        Ok(buf)
303    }
304
305    /// Fallible Describe message encoder.
306    pub fn try_encode_describe(is_portal: bool, name: &str) -> Result<BytesMut, EncodeError> {
307        if Self::has_nul(name) {
308            return Err(EncodeError::NullByte);
309        }
310
311        let mut buf = BytesMut::new();
312        buf.extend_from_slice(b"D");
313
314        let mut content = Vec::new();
315        content.push(if is_portal { b'P' } else { b'S' });
316        content.extend_from_slice(name.as_bytes());
317        content.push(0);
318
319        let len = Self::content_len_to_wire_len(content.len())?;
320        buf.extend_from_slice(&len.to_be_bytes());
321        buf.extend_from_slice(&content);
322        Ok(buf)
323    }
324
325    /// Encode a complete extended query pipeline (OPTIMIZED).
326    /// This combines Parse + Bind + Execute + Sync in a single buffer.
327    /// Zero intermediate allocations - writes directly to pre-sized BytesMut.
328    pub fn encode_extended_query(
329        sql: &str,
330        params: &[Option<Vec<u8>>],
331    ) -> Result<BytesMut, EncodeError> {
332        Self::encode_extended_query_with_result_format(sql, params, Self::FORMAT_TEXT)
333    }
334
335    /// Encode a complete extended query pipeline with explicit result format.
336    ///
337    /// `result_format` is PostgreSQL wire format code: `0 = text`, `1 = binary`.
338    pub fn encode_extended_query_with_result_format(
339        sql: &str,
340        params: &[Option<Vec<u8>>],
341        result_format: i16,
342    ) -> Result<BytesMut, EncodeError> {
343        Self::encode_extended_query_with_formats(sql, params, Self::FORMAT_TEXT, result_format)
344    }
345
346    /// Encode a complete extended query pipeline with explicit parameter and result formats.
347    ///
348    /// `param_format` / `result_format` are PostgreSQL wire format codes:
349    /// `0 = text`, `1 = binary`.
350    pub fn encode_extended_query_with_formats(
351        sql: &str,
352        params: &[Option<Vec<u8>>],
353        param_format: i16,
354        result_format: i16,
355    ) -> Result<BytesMut, EncodeError> {
356        if Self::has_nul(sql) {
357            return Err(EncodeError::NullByte);
358        }
359        if params.len() > i16::MAX as usize {
360            return Err(EncodeError::TooManyParameters(params.len()));
361        }
362
363        // Calculate total size upfront to avoid reallocations
364        // Bind: 1 + 4 + 1 + 1 + param_formats + 2 + params_data + result_formats
365        // Execute: 1 + 4 + 1 + 4 = 10
366        // Sync: 5
367        let params_size = params.iter().try_fold(0usize, |acc, p| {
368            let field_size = 4usize
369                .checked_add(p.as_ref().map_or(0usize, |v| v.len()))
370                .ok_or(EncodeError::MessageTooLarge(usize::MAX))?;
371            acc.checked_add(field_size)
372                .ok_or(EncodeError::MessageTooLarge(usize::MAX))
373        })?;
374        let param_formats_size = Self::param_format_wire_len(param_format);
375        let result_formats_size = Self::result_format_wire_len(result_format);
376        let total_size = 9usize
377            .checked_add(sql.len())
378            .and_then(|v| v.checked_add(9))
379            .and_then(|v| v.checked_add(params_size))
380            .and_then(|v| v.checked_add(param_formats_size))
381            .and_then(|v| v.checked_add(result_formats_size))
382            .and_then(|v| v.checked_add(10))
383            .and_then(|v| v.checked_add(5))
384            .ok_or(EncodeError::MessageTooLarge(usize::MAX))?;
385
386        let mut buf = BytesMut::with_capacity(total_size);
387
388        // ===== PARSE =====
389        buf.extend_from_slice(b"P");
390        let parse_content_len = 1usize
391            .checked_add(sql.len())
392            .and_then(|v| v.checked_add(1))
393            .and_then(|v| v.checked_add(2))
394            .ok_or(EncodeError::MessageTooLarge(usize::MAX))?;
395        let parse_len = Self::content_len_to_wire_len(parse_content_len)?;
396        buf.extend_from_slice(&parse_len.to_be_bytes());
397        buf.extend_from_slice(&[0]); // Unnamed statement
398        buf.extend_from_slice(sql.as_bytes());
399        buf.extend_from_slice(&[0]); // Null terminator
400        buf.extend_from_slice(&0i16.to_be_bytes()); // No param types (infer)
401
402        // ===== BIND =====
403        buf.extend_from_slice(b"B");
404        let bind_content_len = 1usize
405            .checked_add(1)
406            .and_then(|v| v.checked_add(2))
407            .and_then(|v| v.checked_add(param_formats_size))
408            .and_then(|v| v.checked_add(params_size))
409            .and_then(|v| v.checked_add(result_formats_size))
410            .ok_or(EncodeError::MessageTooLarge(usize::MAX))?;
411        let bind_len = Self::content_len_to_wire_len(bind_content_len)?;
412        buf.extend_from_slice(&bind_len.to_be_bytes());
413        buf.extend_from_slice(&[0]); // Unnamed portal
414        buf.extend_from_slice(&[0]); // Unnamed statement
415        Self::encode_param_formats_bytesmut(&mut buf, param_format);
416        let param_count = Self::usize_to_i16(params.len())?;
417        buf.extend_from_slice(&param_count.to_be_bytes());
418        for param in params {
419            match param {
420                None => buf.extend_from_slice(&(-1i32).to_be_bytes()),
421                Some(data) => {
422                    let data_len = Self::usize_to_i32(data.len())?;
423                    buf.extend_from_slice(&data_len.to_be_bytes());
424                    buf.extend_from_slice(data);
425                }
426            }
427        }
428        Self::encode_result_formats_bytesmut(&mut buf, result_format);
429
430        // ===== EXECUTE =====
431        buf.extend_from_slice(b"E");
432        buf.extend_from_slice(&9i32.to_be_bytes()); // len = 4 + 1 + 4
433        buf.extend_from_slice(&[0]); // Unnamed portal
434        buf.extend_from_slice(&0i32.to_be_bytes()); // Unlimited rows
435
436        // ===== SYNC =====
437        buf.extend_from_slice(&[b'S', 0, 0, 0, 4]);
438
439        Ok(buf)
440    }
441
442    /// Fallible CopyFail encoder.
443    pub fn try_encode_copy_fail(reason: &str) -> Result<BytesMut, EncodeError> {
444        if Self::has_nul(reason) {
445            return Err(EncodeError::NullByte);
446        }
447
448        let mut buf = BytesMut::new();
449        buf.extend_from_slice(b"f");
450        let content_len = reason.len() + 1; // +1 for null terminator
451        let len = Self::content_len_to_wire_len(content_len)?;
452        buf.extend_from_slice(&len.to_be_bytes());
453        buf.extend_from_slice(reason.as_bytes());
454        buf.extend_from_slice(&[0]);
455        Ok(buf)
456    }
457
458    /// Fallible Close encoder.
459    pub fn try_encode_close(is_portal: bool, name: &str) -> Result<BytesMut, EncodeError> {
460        if Self::has_nul(name) {
461            return Err(EncodeError::NullByte);
462        }
463
464        let mut buf = BytesMut::new();
465        buf.extend_from_slice(b"C");
466        let content_len = 1 + name.len() + 1; // type + name + null
467        let len = Self::content_len_to_wire_len(content_len)?;
468        buf.extend_from_slice(&len.to_be_bytes());
469        buf.extend_from_slice(&[if is_portal { b'P' } else { b'S' }]);
470        buf.extend_from_slice(name.as_bytes());
471        buf.extend_from_slice(&[0]);
472        Ok(buf)
473    }
474}
475
476// ==================== ULTRA-OPTIMIZED Hot Path Encoders ====================
477//
478// These encoders are designed to beat C:
479// - Direct integer writes (no temp arrays, no bounds checks)
480// - Borrowed slice params (zero-copy)
481// - Single store instructions via BufMut
482//
483
484use bytes::BufMut;
485
486/// Zero-copy parameter for ultra-fast encoding.
487/// Uses borrowed slices to avoid any allocation or copy.
488pub enum Param<'a> {
489    /// SQL NULL value.
490    Null,
491    /// Non-null parameter as a borrowed byte slice.
492    Bytes(&'a [u8]),
493}
494
495impl PgEncoder {
496    /// Direct i32 write - no temp array, no bounds check.
497    /// LLVM emits a single store instruction.
498    #[inline(always)]
499    fn put_i32_be(buf: &mut BytesMut, v: i32) {
500        buf.put_i32(v);
501    }
502
503    #[inline(always)]
504    fn put_i16_be(buf: &mut BytesMut, v: i16) {
505        buf.put_i16(v);
506    }
507
508    /// Encode Bind message - ULTRA OPTIMIZED.
509    /// - Direct integer writes (no temp arrays)
510    /// - Borrowed params (zero-copy)
511    /// - Single allocation check
512    #[inline]
513    pub fn encode_bind_ultra<'a>(
514        buf: &mut BytesMut,
515        statement: &str,
516        params: &[Param<'a>],
517    ) -> Result<(), EncodeError> {
518        Self::encode_bind_ultra_with_result_format(buf, statement, params, Self::FORMAT_TEXT)
519    }
520
521    /// Encode Bind message with explicit result-column format.
522    #[inline]
523    pub fn encode_bind_ultra_with_result_format<'a>(
524        buf: &mut BytesMut,
525        statement: &str,
526        params: &[Param<'a>],
527        result_format: i16,
528    ) -> Result<(), EncodeError> {
529        Self::encode_bind_ultra_with_formats(
530            buf,
531            statement,
532            params,
533            Self::FORMAT_TEXT,
534            result_format,
535        )
536    }
537
538    /// Encode Bind message with explicit parameter and result format codes.
539    #[inline]
540    pub fn encode_bind_ultra_with_formats<'a>(
541        buf: &mut BytesMut,
542        statement: &str,
543        params: &[Param<'a>],
544        param_format: i16,
545        result_format: i16,
546    ) -> Result<(), EncodeError> {
547        if Self::has_nul(statement) {
548            return Err(EncodeError::NullByte);
549        }
550        if params.len() > i16::MAX as usize {
551            return Err(EncodeError::TooManyParameters(params.len()));
552        }
553
554        // Calculate content length upfront
555        let params_size = params.iter().try_fold(0usize, |acc, p| {
556            let field_size = match p {
557                Param::Null => 4usize,
558                Param::Bytes(b) => 4usize
559                    .checked_add(b.len())
560                    .ok_or(EncodeError::MessageTooLarge(usize::MAX))?,
561            };
562            acc.checked_add(field_size)
563                .ok_or(EncodeError::MessageTooLarge(usize::MAX))
564        })?;
565        let param_formats_size = Self::param_format_wire_len(param_format);
566        let result_formats_size = Self::result_format_wire_len(result_format);
567        let content_len = 1usize
568            .checked_add(statement.len())
569            .and_then(|v| v.checked_add(1))
570            .and_then(|v| v.checked_add(2))
571            .and_then(|v| v.checked_add(param_formats_size))
572            .and_then(|v| v.checked_add(params_size))
573            .and_then(|v| v.checked_add(result_formats_size))
574            .ok_or(EncodeError::MessageTooLarge(usize::MAX))?;
575        let wire_len = Self::content_len_to_wire_len(content_len)?;
576
577        // Single reserve - no more allocations
578        buf.reserve(1 + 4 + content_len);
579
580        // Message type 'B'
581        buf.put_u8(b'B');
582
583        // Length (includes itself) - DIRECT WRITE
584        Self::put_i32_be(buf, wire_len);
585
586        // Portal name (empty, null-terminated)
587        buf.put_u8(0);
588
589        // Statement name (null-terminated)
590        buf.extend_from_slice(statement.as_bytes());
591        buf.put_u8(0);
592
593        // Parameter format codes
594        Self::encode_param_formats_bytesmut(buf, param_format);
595
596        // Parameter count
597        let param_count = Self::usize_to_i16(params.len())?;
598        Self::put_i16_be(buf, param_count);
599
600        // Parameters - ZERO COPY from borrowed slices
601        for param in params {
602            match param {
603                Param::Null => Self::put_i32_be(buf, -1),
604                Param::Bytes(data) => {
605                    let data_len = Self::usize_to_i32(data.len())?;
606                    Self::put_i32_be(buf, data_len);
607                    buf.extend_from_slice(data);
608                }
609            }
610        }
611
612        // Result format codes
613        Self::encode_result_formats_bytesmut(buf, result_format);
614        Ok(())
615    }
616
617    /// Encode Execute message - ULTRA OPTIMIZED.
618    #[inline(always)]
619    pub fn encode_execute_ultra(buf: &mut BytesMut) {
620        // Execute: 'E' + len(9) + portal("") + max_rows(0)
621        // = 'E' 00 00 00 09 00 00 00 00 00
622        buf.extend_from_slice(&[b'E', 0, 0, 0, 9, 0, 0, 0, 0, 0]);
623    }
624
625    /// Encode Sync message - ULTRA OPTIMIZED.
626    #[inline(always)]
627    pub fn encode_sync_ultra(buf: &mut BytesMut) {
628        buf.extend_from_slice(&[b'S', 0, 0, 0, 4]);
629    }
630
631    // Keep the original methods for compatibility
632
633    /// Encode Bind message directly into existing buffer (ZERO ALLOCATION).
634    /// This is the hot path optimization - no intermediate Vec allocation.
635    #[inline]
636    pub fn encode_bind_to(
637        buf: &mut BytesMut,
638        statement: &str,
639        params: &[Option<Vec<u8>>],
640    ) -> Result<(), EncodeError> {
641        Self::encode_bind_to_with_result_format(buf, statement, params, Self::FORMAT_TEXT)
642    }
643
644    /// Encode Bind into existing buffer with explicit result-column format.
645    #[inline]
646    pub fn encode_bind_to_with_result_format(
647        buf: &mut BytesMut,
648        statement: &str,
649        params: &[Option<Vec<u8>>],
650        result_format: i16,
651    ) -> Result<(), EncodeError> {
652        Self::encode_bind_to_with_formats(buf, statement, params, Self::FORMAT_TEXT, result_format)
653    }
654
655    /// Encode Bind into existing buffer with explicit parameter and result formats.
656    #[inline]
657    pub fn encode_bind_to_with_formats(
658        buf: &mut BytesMut,
659        statement: &str,
660        params: &[Option<Vec<u8>>],
661        param_format: i16,
662        result_format: i16,
663    ) -> Result<(), EncodeError> {
664        if Self::has_nul(statement) {
665            return Err(EncodeError::NullByte);
666        }
667        if params.len() > i16::MAX as usize {
668            return Err(EncodeError::TooManyParameters(params.len()));
669        }
670
671        // Calculate content length upfront
672        // portal(1) + statement(len+1) + param_formats + param_count(2)
673        // + params_data + result_formats(2 or 4)
674        let params_size = params.iter().try_fold(0usize, |acc, p| {
675            let field_size = 4usize
676                .checked_add(p.as_ref().map_or(0usize, |v| v.len()))
677                .ok_or(EncodeError::MessageTooLarge(usize::MAX))?;
678            acc.checked_add(field_size)
679                .ok_or(EncodeError::MessageTooLarge(usize::MAX))
680        })?;
681        let param_formats_size = Self::param_format_wire_len(param_format);
682        let result_formats_size = Self::result_format_wire_len(result_format);
683        let content_len = 1usize
684            .checked_add(statement.len())
685            .and_then(|v| v.checked_add(1))
686            .and_then(|v| v.checked_add(2))
687            .and_then(|v| v.checked_add(param_formats_size))
688            .and_then(|v| v.checked_add(params_size))
689            .and_then(|v| v.checked_add(result_formats_size))
690            .ok_or(EncodeError::MessageTooLarge(usize::MAX))?;
691        let wire_len = Self::content_len_to_wire_len(content_len)?;
692
693        buf.reserve(1 + 4 + content_len);
694
695        // Message type 'B'
696        buf.put_u8(b'B');
697
698        // Length (includes itself) - DIRECT WRITE
699        Self::put_i32_be(buf, wire_len);
700
701        // Portal name (empty, null-terminated)
702        buf.put_u8(0);
703
704        // Statement name (null-terminated)
705        buf.extend_from_slice(statement.as_bytes());
706        buf.put_u8(0);
707
708        // Parameter format codes
709        Self::encode_param_formats_bytesmut(buf, param_format);
710
711        // Parameter count
712        let param_count = Self::usize_to_i16(params.len())?;
713        Self::put_i16_be(buf, param_count);
714
715        // Parameters
716        for param in params {
717            match param {
718                None => Self::put_i32_be(buf, -1),
719                Some(data) => {
720                    let data_len = Self::usize_to_i32(data.len())?;
721                    Self::put_i32_be(buf, data_len);
722                    buf.extend_from_slice(data);
723                }
724            }
725        }
726
727        // Result format codes
728        Self::encode_result_formats_bytesmut(buf, result_format);
729        Ok(())
730    }
731
732    /// Encode Execute message directly into existing buffer (ZERO ALLOCATION).
733    #[inline]
734    pub fn encode_execute_to(buf: &mut BytesMut) {
735        // Content: portal(1) + max_rows(4) = 5 bytes
736        buf.extend_from_slice(&[b'E', 0, 0, 0, 9, 0, 0, 0, 0, 0]);
737    }
738
739    /// Encode Sync message directly into existing buffer (ZERO ALLOCATION).
740    #[inline]
741    pub fn encode_sync_to(buf: &mut BytesMut) {
742        buf.extend_from_slice(&[b'S', 0, 0, 0, 4]);
743    }
744}
745
746#[cfg(test)]
747mod tests {
748    use super::*;
749
750    // NOTE: test_encode_simple_query removed - use AstEncoder instead
751    #[test]
752    fn test_encode_query_string() {
753        let sql = "SELECT 1";
754        let bytes = PgEncoder::try_encode_query_string(sql).unwrap();
755
756        // Message type
757        assert_eq!(bytes[0], b'Q');
758
759        // Length: 4 (length field) + 8 (query) + 1 (null) = 13
760        let len = i32::from_be_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]);
761        assert_eq!(len, 13);
762
763        // Query content
764        assert_eq!(&bytes[5..13], b"SELECT 1");
765
766        // Null terminator
767        assert_eq!(bytes[13], 0);
768    }
769
770    #[test]
771    fn test_encode_terminate() {
772        let bytes = PgEncoder::encode_terminate();
773        assert_eq!(bytes.as_ref(), &[b'X', 0, 0, 0, 4]);
774    }
775
776    #[test]
777    fn test_encode_sync() {
778        let bytes = PgEncoder::encode_sync();
779        assert_eq!(bytes.as_ref(), &[b'S', 0, 0, 0, 4]);
780    }
781
782    #[test]
783    fn test_encode_parse() {
784        let bytes = PgEncoder::try_encode_parse("", "SELECT $1", &[]).unwrap();
785
786        // Message type 'P'
787        assert_eq!(bytes[0], b'P');
788
789        // Content should include query
790        let content = String::from_utf8_lossy(&bytes[5..]);
791        assert!(content.contains("SELECT $1"));
792    }
793
794    #[test]
795    fn test_encode_bind() {
796        let params = vec![
797            Some(b"42".to_vec()),
798            None, // NULL
799        ];
800        let bytes = PgEncoder::encode_bind("", "", &params).unwrap();
801
802        // Message type 'B'
803        assert_eq!(bytes[0], b'B');
804
805        // Should have proper length
806        let len = i32::from_be_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]);
807        assert!(len > 4); // At least header
808    }
809
810    #[test]
811    fn test_encode_bind_binary_result_format() {
812        let bytes =
813            PgEncoder::encode_bind_with_result_format("", "", &[], PgEncoder::FORMAT_BINARY)
814                .unwrap();
815
816        // B + len + portal + statement + param formats + param count + result formats
817        // Result format section for binary should be: count=1, format=1.
818        assert_eq!(&bytes[11..15], &[0, 1, 0, 1]);
819    }
820
821    #[test]
822    fn test_encode_bind_binary_param_and_result_format() {
823        let bytes = PgEncoder::encode_bind_with_formats(
824            "",
825            "",
826            &[],
827            PgEncoder::FORMAT_BINARY,
828            PgEncoder::FORMAT_BINARY,
829        )
830        .unwrap();
831
832        // portal, statement, param formats(count+code), param count, result formats(count+code)
833        assert_eq!(&bytes[7..11], &[0, 1, 0, 1]);
834        assert_eq!(&bytes[11..13], &[0, 0]);
835        assert_eq!(&bytes[13..17], &[0, 1, 0, 1]);
836    }
837
838    #[test]
839    fn test_encode_execute() {
840        let bytes = PgEncoder::try_encode_execute("", 0).unwrap();
841
842        // Message type 'E'
843        assert_eq!(bytes[0], b'E');
844
845        // Length: 4 + 1 (null) + 4 (max_rows) = 9
846        let len = i32::from_be_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]);
847        assert_eq!(len, 9);
848    }
849
850    #[test]
851    fn test_encode_execute_negative_max_rows_returns_error() {
852        let err = PgEncoder::try_encode_execute("", -1).expect_err("must reject negative max_rows");
853        assert_eq!(err, EncodeError::InvalidMaxRows(-1));
854    }
855
856    #[test]
857    fn test_encode_extended_query() {
858        let params = vec![Some(b"hello".to_vec())];
859        let bytes = PgEncoder::encode_extended_query("SELECT $1", &params).unwrap();
860
861        // Should contain all 4 message types: P, B, E, S
862        assert!(bytes.windows(1).any(|w| w == [b'P']));
863        assert!(bytes.windows(1).any(|w| w == [b'B']));
864        assert!(bytes.windows(1).any(|w| w == [b'E']));
865        assert!(bytes.windows(1).any(|w| w == [b'S']));
866    }
867
868    #[test]
869    fn test_encode_extended_query_binary_result_format() {
870        let bytes = PgEncoder::encode_extended_query_with_result_format(
871            "SELECT 1",
872            &[],
873            PgEncoder::FORMAT_BINARY,
874        )
875        .unwrap();
876
877        let parse_len = i32::from_be_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]) as usize;
878        let bind_start = 1 + parse_len;
879        assert_eq!(bytes[bind_start], b'B');
880
881        let bind_len = i32::from_be_bytes([
882            bytes[bind_start + 1],
883            bytes[bind_start + 2],
884            bytes[bind_start + 3],
885            bytes[bind_start + 4],
886        ]);
887        assert_eq!(bind_len, 14);
888
889        let bind_content = &bytes[bind_start + 5..bind_start + 1 + bind_len as usize];
890        assert_eq!(&bind_content[6..10], &[0, 1, 0, 1]);
891    }
892
893    #[test]
894    fn test_encode_extended_query_binary_param_and_result_format() {
895        let bytes = PgEncoder::encode_extended_query_with_formats(
896            "SELECT 1",
897            &[],
898            PgEncoder::FORMAT_BINARY,
899            PgEncoder::FORMAT_BINARY,
900        )
901        .unwrap();
902
903        let parse_len = i32::from_be_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]) as usize;
904        let bind_start = 1 + parse_len;
905        let bind_len = i32::from_be_bytes([
906            bytes[bind_start + 1],
907            bytes[bind_start + 2],
908            bytes[bind_start + 3],
909            bytes[bind_start + 4],
910        ]);
911        assert_eq!(bind_len, 16);
912
913        let bind_content = &bytes[bind_start + 5..bind_start + 1 + bind_len as usize];
914        assert_eq!(&bind_content[2..6], &[0, 1, 0, 1]);
915        assert_eq!(&bind_content[6..8], &[0, 0]);
916        assert_eq!(&bind_content[8..12], &[0, 1, 0, 1]);
917    }
918
919    #[test]
920    fn test_encode_copy_fail() {
921        let bytes = PgEncoder::try_encode_copy_fail("bad data").unwrap();
922        assert_eq!(bytes[0], b'f');
923        let len = i32::from_be_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]);
924        assert_eq!(len as usize, 4 + "bad data".len() + 1);
925        assert_eq!(&bytes[5..13], b"bad data");
926        assert_eq!(bytes[13], 0);
927    }
928
929    #[test]
930    fn test_encode_close_statement() {
931        let bytes = PgEncoder::try_encode_close(false, "my_stmt").unwrap();
932        assert_eq!(bytes[0], b'C');
933        assert_eq!(bytes[5], b'S'); // Statement type
934        assert_eq!(&bytes[6..13], b"my_stmt");
935        assert_eq!(bytes[13], 0);
936    }
937
938    #[test]
939    fn test_encode_close_portal() {
940        let bytes = PgEncoder::try_encode_close(true, "").unwrap();
941        assert_eq!(bytes[0], b'C');
942        assert_eq!(bytes[5], b'P'); // Portal type
943        assert_eq!(bytes[6], 0); // Empty name null terminator
944    }
945
946    #[test]
947    fn test_encode_parse_too_many_param_types_returns_error() {
948        let param_types = vec![0u32; (i16::MAX as usize) + 1];
949        let err =
950            PgEncoder::try_encode_parse("s", "SELECT 1", &param_types).expect_err("must reject");
951        assert_eq!(err, EncodeError::TooManyParameters(param_types.len()));
952    }
953
954    #[test]
955    fn test_encode_bind_to_binary_result_format() {
956        let mut buf = BytesMut::new();
957        PgEncoder::encode_bind_to_with_result_format(&mut buf, "", &[], PgEncoder::FORMAT_BINARY)
958            .unwrap();
959
960        assert_eq!(&buf[11..15], &[0, 1, 0, 1]);
961    }
962
963    #[test]
964    fn test_encode_bind_to_binary_param_and_result_format() {
965        let mut buf = BytesMut::new();
966        PgEncoder::encode_bind_to_with_formats(
967            &mut buf,
968            "",
969            &[],
970            PgEncoder::FORMAT_BINARY,
971            PgEncoder::FORMAT_BINARY,
972        )
973        .unwrap();
974
975        assert_eq!(&buf[7..11], &[0, 1, 0, 1]);
976        assert_eq!(&buf[11..13], &[0, 0]);
977        assert_eq!(&buf[13..17], &[0, 1, 0, 1]);
978    }
979
980    #[test]
981    fn test_encode_bind_ultra_binary_result_format() {
982        let mut buf = BytesMut::new();
983        PgEncoder::encode_bind_ultra_with_result_format(
984            &mut buf,
985            "",
986            &[],
987            PgEncoder::FORMAT_BINARY,
988        )
989        .unwrap();
990
991        assert_eq!(&buf[11..15], &[0, 1, 0, 1]);
992    }
993
994    #[test]
995    fn test_encode_bind_ultra_binary_param_and_result_format() {
996        let mut buf = BytesMut::new();
997        PgEncoder::encode_bind_ultra_with_formats(
998            &mut buf,
999            "",
1000            &[],
1001            PgEncoder::FORMAT_BINARY,
1002            PgEncoder::FORMAT_BINARY,
1003        )
1004        .unwrap();
1005
1006        assert_eq!(&buf[7..11], &[0, 1, 0, 1]);
1007        assert_eq!(&buf[11..13], &[0, 0]);
1008        assert_eq!(&buf[13..17], &[0, 1, 0, 1]);
1009    }
1010
1011    #[test]
1012    fn test_encode_query_string_with_nul_returns_empty() {
1013        let err =
1014            PgEncoder::try_encode_query_string("select 1\0select 2").expect_err("must reject NUL");
1015        assert_eq!(err, EncodeError::NullByte);
1016    }
1017
1018    #[test]
1019    fn test_encode_parse_with_nul_returns_empty() {
1020        let err = PgEncoder::try_encode_parse("s", "SELECT 1\0", &[]).expect_err("must reject");
1021        assert_eq!(err, EncodeError::NullByte);
1022    }
1023
1024    #[test]
1025    fn test_encode_bind_with_nul_rejected() {
1026        let err = PgEncoder::encode_bind_with_result_format("\0", "", &[], PgEncoder::FORMAT_TEXT)
1027            .expect_err("bind with NUL portal must fail");
1028        assert_eq!(err, EncodeError::NullByte);
1029    }
1030
1031    #[test]
1032    fn test_encode_extended_query_with_nul_rejected() {
1033        let err = PgEncoder::encode_extended_query_with_result_format(
1034            "SELECT 1\0UNION SELECT 2",
1035            &[],
1036            PgEncoder::FORMAT_TEXT,
1037        )
1038        .expect_err("extended query with NUL SQL must fail");
1039        assert_eq!(err, EncodeError::NullByte);
1040    }
1041
1042    #[test]
1043    fn test_encode_copy_fail_with_nul_returns_empty() {
1044        let err = PgEncoder::try_encode_copy_fail("bad\0data").expect_err("must reject");
1045        assert_eq!(err, EncodeError::NullByte);
1046    }
1047}