qail_pg/driver/
prepared.rs

1//! High-performance prepared statement handling.
2//!
3//! This module provides zero-allocation prepared statement caching
4//! to match Go pgx performance.
5
6use std::collections::hash_map::DefaultHasher;
7use std::hash::{Hash, Hasher};
8
9/// A prepared statement handle with pre-computed statement name.
10/// This eliminates per-query hash computation and HashMap lookup.
11/// Create once, execute many times.
12/// # Example
13/// ```ignore
14/// // Prepare once (compute hash + register with PostgreSQL)
15/// let stmt = conn.prepare("SELECT id, name FROM users WHERE id = $1").await?;
16/// // Execute many times (no hash, no lookup!)
17/// for id in 1..1000 {
18///     conn.execute_prepared(&stmt, &[Some(id.to_string().into_bytes())]).await?;
19/// }
20/// ```
21#[derive(Clone, Debug)]
22pub struct PreparedStatement {
23    /// Pre-computed statement name (e.g., "s1234567890abcdef")
24    pub(crate) name: String,
25    #[allow(dead_code)]
26    pub(crate) param_count: usize,
27}
28
29impl PreparedStatement {
30    /// Create a new prepared statement handle from SQL bytes.
31    /// This hashes the SQL bytes directly without String allocation.
32    #[inline]
33    pub fn from_sql_bytes(sql_bytes: &[u8]) -> Self {
34        let name = sql_bytes_to_stmt_name(sql_bytes);
35        // Count $N placeholders (simple heuristic)
36        let param_count = sql_bytes
37            .windows(2)
38            .filter(|w| w[0] == b'$' && w[1].is_ascii_digit())
39            .count();
40        Self { name, param_count }
41    }
42
43    /// Create from SQL string (convenience method).
44    #[inline]
45    pub fn from_sql(sql: &str) -> Self {
46        Self::from_sql_bytes(sql.as_bytes())
47    }
48
49    /// Get the statement name.
50    #[inline]
51    pub fn name(&self) -> &str {
52        &self.name
53    }
54}
55
56/// Hash SQL bytes directly to statement name (no String allocation).
57/// This is faster than hashing a String because:
58/// 1. No UTF-8 validation
59/// 2. No heap allocation for String
60/// 3. Direct byte hashing
61#[inline]
62pub fn sql_bytes_to_stmt_name(sql: &[u8]) -> String {
63    let mut hasher = DefaultHasher::new();
64    sql.hash(&mut hasher);
65    format!("s{:016x}", hasher.finish())
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn test_stmt_name_from_bytes() {
74        let sql = b"SELECT id, name FROM users WHERE id = $1";
75        let name1 = sql_bytes_to_stmt_name(sql);
76        let name2 = sql_bytes_to_stmt_name(sql);
77        assert_eq!(name1, name2); // Deterministic
78        assert!(name1.starts_with("s"));
79        assert_eq!(name1.len(), 17); // "s" + 16 hex chars
80    }
81
82    #[test]
83    fn test_prepared_statement() {
84        let stmt = PreparedStatement::from_sql("SELECT * FROM users WHERE id = $1 AND name = $2");
85        assert_eq!(stmt.param_count, 2);
86        assert!(stmt.name.starts_with("s"));
87    }
88}