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