Skip to main content

radixdb_api/
statement.rs

1// Copyright 2026 RadixDB Contributors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Prepared statement support
16//!
17//! # Examples
18//!
19//! ```no_run
20//! use radixdb_api::Database;
21//! # fn main() -> radixdb_core::Result<()> {
22//!
23//! let db = Database::open("memory://")?;
24//! db.execute("CREATE TABLE users (id INTEGER, name TEXT)", ())?;
25//!
26//! // Prepare a statement for repeated execution
27//! let insert = db.prepare("INSERT INTO users VALUES ($1, $2)")?;
28//!
29//! // Execute multiple times efficiently
30//! for (id, name) in [(1, "Alice"), (2, "Bob"), (3, "Charlie")] {
31//!     insert.execute((id, name))?;
32//! }
33//!
34//! // Prepare a query
35//! let select = db.prepare("SELECT name FROM users WHERE id = $1")?;
36//! for id in 1..=3 {
37//!     for row in select.query((id,))? {
38//!         println!("{}", row?.get::<String>(0)?);
39//!     }
40//! }
41//! # Ok(())
42//! # }
43//! ```
44
45use std::sync::{Arc, Weak};
46
47use radixdb_core::{Error, Result};
48use radixdb_executor::context::ExecutionContext;
49use radixdb_executor::PreparedProgram;
50
51use super::database::{Database, DatabaseInnerHandle, FromValue};
52use super::params::Params;
53use super::rows::Rows;
54
55/// A prepared SQL statement
56///
57/// Prepared statements parse SQL once at prepare time and retain the compiled
58/// plan. Subsequent executions use the plan directly, bypassing normalize,
59/// hash, and cache-lookup overhead on every call.
60///
61/// # Thread Safety
62///
63/// Statement holds a weak reference to the Database and can be used from
64/// multiple threads, but each execution is serialized through the
65/// database's executor lock.
66///
67/// # Lifetime
68///
69/// The Statement becomes invalid when the Database is dropped. Attempting
70/// to use a Statement after its Database is dropped will return an error.
71#[derive(Clone)]
72pub struct Statement {
73    /// Weak reference to database - doesn't prevent cleanup
74    db_weak: Weak<DatabaseInnerHandle>,
75    sql: String,
76    program: PreparedProgram,
77}
78
79impl Statement {
80    /// Create a new prepared statement.
81    ///
82    /// Parses the SQL and retains the compiled plan so that subsequent
83    /// executions bypass cache lookup entirely. Returns an error if the
84    /// SQL is invalid.
85    pub(crate) fn new(
86        db_weak: Weak<DatabaseInnerHandle>,
87        sql: String,
88        db: &Database,
89    ) -> Result<Self> {
90        let program = {
91            let executor = db
92                .executor()
93                .lock()
94                .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
95            executor.prepare_program(&sql)?
96        };
97
98        Ok(Self {
99            db_weak,
100            sql,
101            program,
102        })
103    }
104
105    /// Get the database, upgrading the weak reference.
106    /// Returns an error if the database was dropped.
107    #[inline]
108    fn get_db(&self) -> Result<Database> {
109        let db = self
110            .db_weak
111            .upgrade()
112            .map(Database::from_inner)
113            .ok_or_else(|| Error::internal("Database was dropped"))?;
114        db.ensure_open()?;
115        Ok(db)
116    }
117
118    pub(crate) fn validate_owner(&self, owner: &Arc<DatabaseInnerHandle>) -> Result<()> {
119        let actual = self
120            .db_weak
121            .upgrade()
122            .ok_or_else(|| Error::internal("Database was dropped"))?;
123        if Arc::ptr_eq(&actual, owner) {
124            Database::from_inner(actual).ensure_open()
125        } else {
126            Err(Error::invalid_argument(
127                "prepared Statement belongs to a different Database connection",
128            ))
129        }
130    }
131
132    pub(crate) fn prepared_program(&self) -> &PreparedProgram {
133        &self.program
134    }
135
136    /// Execute the prepared statement
137    ///
138    /// Returns the number of rows affected for DML statements.
139    ///
140    /// # Examples
141    ///
142    /// ```ignore
143    /// let stmt = db.prepare("INSERT INTO users VALUES ($1, $2)")?;
144    /// stmt.execute((1, "Alice"))?;
145    /// stmt.execute((2, "Bob"))?;
146    /// ```
147    pub fn execute<P: Params>(&self, params: P) -> Result<i64> {
148        let db = self.get_db()?;
149        let executor = db
150            .executor()
151            .lock()
152            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
153        let ctx = ExecutionContext::with_params(params.into_params());
154        let result = executor.execute_prepared_program(&self.program, &ctx)?;
155        Ok(result.rows_affected())
156    }
157
158    /// Query using the prepared statement
159    ///
160    /// Returns an iterator over the result rows.
161    ///
162    /// # Examples
163    ///
164    /// ```ignore
165    /// let stmt = db.prepare("SELECT * FROM users WHERE age > $1")?;
166    ///
167    /// for row in stmt.query((18,))? {
168    ///     let row = row?;
169    ///     println!("{}", row.get::<String>("name")?);
170    /// }
171    /// ```
172    pub fn query<P: Params>(&self, params: P) -> Result<Rows> {
173        let db = self.get_db()?;
174        let executor = db
175            .executor()
176            .lock()
177            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
178        let ctx = ExecutionContext::with_params(params.into_params());
179        let result = executor.execute_prepared_program(&self.program, &ctx)?;
180        Ok(Rows::new(result))
181    }
182
183    /// Execute a prepared statement for a network session without exposing
184    /// the executor context type to the server runtime.
185    #[doc(hidden)]
186    pub fn query_for_server(&self, context: super::ServerExecutionContext) -> Result<Rows> {
187        let db = self.get_db()?;
188        let executor = db
189            .executor()
190            .lock()
191            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
192        let result = executor.execute_prepared_program(&self.program, context.inner())?;
193        Ok(Rows::new(result))
194    }
195
196    /// Query and return a single value
197    ///
198    /// # Examples
199    ///
200    /// ```ignore
201    /// let stmt = db.prepare("SELECT name FROM users WHERE id = $1")?;
202    /// let name: String = stmt.query_one((1,))?;
203    /// ```
204    pub fn query_one<T: FromValue, P: Params>(&self, params: P) -> Result<T> {
205        let row = self.query(params)?.next().ok_or(Error::NoRowsReturned)??;
206        row.get(0)
207    }
208
209    /// Query and return an optional single value
210    ///
211    /// # Examples
212    ///
213    /// ```ignore
214    /// let stmt = db.prepare("SELECT name FROM users WHERE id = $1")?;
215    /// let name: Option<String> = stmt.query_opt((999,))?;
216    /// ```
217    pub fn query_opt<T: FromValue, P: Params>(&self, params: P) -> Result<Option<T>> {
218        match self.query(params)?.next() {
219            None => Ok(None),
220            Some(Err(e)) => Err(e),
221            Some(Ok(row)) => Ok(Some(row.get(0)?)),
222        }
223    }
224
225    /// Get the SQL text of this statement
226    pub fn sql(&self) -> &str {
227        &self.sql
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    #[test]
236    fn test_prepared_statement_execute() {
237        let db = Database::open_in_memory().unwrap();
238        db.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)", ())
239            .unwrap();
240
241        let stmt = db.prepare("INSERT INTO users VALUES ($1, $2)").unwrap();
242
243        stmt.execute((1, "Alice")).unwrap();
244        stmt.execute((2, "Bob")).unwrap();
245        stmt.execute((3, "Charlie")).unwrap();
246
247        let count: i64 = db.query_one("SELECT COUNT(*) FROM users", ()).unwrap();
248        assert_eq!(count, 3);
249    }
250
251    #[test]
252    fn test_prepared_statement_query() {
253        let db = Database::open_in_memory().unwrap();
254        db.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)", ())
255            .unwrap();
256        db.execute(
257            "INSERT INTO users VALUES ($1, $2), ($3, $4), ($5, $6)",
258            (1, "Alice", 2, "Bob", 3, "Charlie"),
259        )
260        .unwrap();
261
262        let stmt = db.prepare("SELECT name FROM users WHERE id = $1").unwrap();
263
264        let name: String = stmt.query_one((1,)).unwrap();
265        assert_eq!(name, "Alice");
266
267        let name: String = stmt.query_one((2,)).unwrap();
268        assert_eq!(name, "Bob");
269
270        let name: String = stmt.query_one((3,)).unwrap();
271        assert_eq!(name, "Charlie");
272    }
273
274    #[test]
275    fn test_prepared_statement_query_opt() {
276        let db = Database::open_in_memory().unwrap();
277        db.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)", ())
278            .unwrap();
279        db.execute("INSERT INTO users VALUES ($1, $2)", (1, "Alice"))
280            .unwrap();
281
282        let stmt = db.prepare("SELECT name FROM users WHERE id = $1").unwrap();
283
284        let name: Option<String> = stmt.query_opt((1,)).unwrap();
285        assert_eq!(name, Some("Alice".to_string()));
286
287        let name: Option<String> = stmt.query_opt((999,)).unwrap();
288        assert_eq!(name, None);
289    }
290
291    #[test]
292    fn test_prepared_statement_sql() {
293        let db = Database::open_in_memory().unwrap();
294        let stmt = db.prepare("SELECT 1").unwrap();
295        assert_eq!(stmt.sql(), "SELECT 1");
296    }
297}