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}