Skip to main content

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
29/// A fully prepared AST query handle.
30///
31/// This stores:
32/// - precomputed prepared statement identity (`stmt`)
33/// - pre-encoded bind parameters (`params`)
34/// - source SQL text (`sql`) for retry re-prepare paths
35///
36/// Use with `PgDriver::fetch_all_prepared_ast()` for the lowest-overhead
37/// repeated execution of an identical AST command.
38#[derive(Clone, Debug)]
39pub struct PreparedAstQuery {
40    pub(crate) stmt: PreparedStatement,
41    pub(crate) params: Vec<Option<Vec<u8>>>,
42    pub(crate) sql: String,
43    pub(crate) sql_hash: u64,
44}
45
46impl PreparedAstQuery {
47    /// Prepared statement name (server-side identity).
48    #[inline]
49    pub fn statement_name(&self) -> &str {
50        self.stmt.name()
51    }
52
53    /// Number of bind parameters encoded in this query.
54    #[inline]
55    pub fn param_count(&self) -> usize {
56        self.params.len()
57    }
58}
59
60impl PreparedStatement {
61    /// Create a new prepared statement handle from SQL bytes.
62    /// This hashes the SQL bytes directly without String allocation.
63    #[inline]
64    pub fn from_sql_bytes(sql_bytes: &[u8]) -> Self {
65        let name = sql_bytes_to_stmt_name(sql_bytes);
66        // Count $N placeholders (simple heuristic)
67        let param_count = sql_bytes
68            .windows(2)
69            .filter(|w| w[0] == b'$' && w[1].is_ascii_digit())
70            .count();
71        Self { name, param_count }
72    }
73
74    /// Create from SQL string (convenience method).
75    #[inline]
76    pub fn from_sql(sql: &str) -> Self {
77        Self::from_sql_bytes(sql.as_bytes())
78    }
79
80    /// Get the statement name.
81    #[inline]
82    pub fn name(&self) -> &str {
83        &self.name
84    }
85}
86
87/// Hash SQL bytes for prepared-statement cache keys.
88#[inline]
89pub fn sql_bytes_hash(sql: &[u8]) -> u64 {
90    let mut hasher = DefaultHasher::new();
91    sql.hash(&mut hasher);
92    hasher.finish()
93}
94
95/// Convert a hashed SQL key into a deterministic statement name.
96#[inline]
97pub fn stmt_name_from_hash(hash: u64) -> String {
98    format!("s{hash:016x}")
99}
100
101/// Hash SQL bytes directly to statement name (no String allocation).
102/// This is faster than hashing a String because:
103/// 1. No UTF-8 validation
104/// 2. No heap allocation for String
105/// 3. Direct byte hashing
106#[inline]
107pub fn sql_bytes_to_stmt_name(sql: &[u8]) -> String {
108    stmt_name_from_hash(sql_bytes_hash(sql))
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_stmt_name_from_bytes() {
117        let sql = b"SELECT id, name FROM users WHERE id = $1";
118        let name1 = sql_bytes_to_stmt_name(sql);
119        let name2 = sql_bytes_to_stmt_name(sql);
120        let hash = sql_bytes_hash(sql);
121        let name3 = stmt_name_from_hash(hash);
122        assert_eq!(name1, name2); // Deterministic
123        assert_eq!(name1, name3);
124        assert!(name1.starts_with("s"));
125        assert_eq!(name1.len(), 17); // "s" + 16 hex chars
126    }
127
128    #[test]
129    fn test_prepared_statement() {
130        let stmt = PreparedStatement::from_sql("SELECT * FROM users WHERE id = $1 AND name = $2");
131        assert_eq!(stmt.param_count, 2);
132        assert!(stmt.name.starts_with("s"));
133    }
134}