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}