Skip to main content

ic_sqlite_vfs/db/
statement.rs

1//! Prepared statement lifecycle and row iteration.
2//!
3//! Each execution resets and rebinds the statement. `Drop` finalizes regular
4//! statements; cached statements return to the connection cache.
5
6use crate::db::connection::sqlite_error;
7use crate::db::row::{FromColumn, Row};
8use crate::db::value::{bind_all_with_count, bind_named_all, ToSql};
9use crate::db::DbError;
10use crate::sqlite_vfs::ffi;
11use std::ffi::c_void;
12use std::ptr::NonNull;
13
14#[cfg(any(test, feature = "canister-api-test-failpoints"))]
15use std::cell::RefCell;
16#[cfg(any(test, feature = "canister-api-test-failpoints"))]
17use std::collections::BTreeMap;
18
19#[cfg(any(test, feature = "canister-api-test-failpoints"))]
20thread_local! {
21    static STEP_FAILPOINTS: RefCell<BTreeMap<crate::stable::memory::ContextId, StepFailpointState>> = const { RefCell::new(BTreeMap::new()) };
22}
23
24#[cfg(any(test, feature = "canister-api-test-failpoints"))]
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub struct StepFailpoint {
27    pub ordinal: u64,
28    pub code: std::ffi::c_int,
29}
30
31#[cfg(any(test, feature = "canister-api-test-failpoints"))]
32#[derive(Clone, Copy, Debug)]
33struct StepFailpointState {
34    failpoint: StepFailpoint,
35    count: u64,
36}
37
38pub struct Statement<'connection> {
39    db: *mut ffi::sqlite3,
40    raw: NonNull<ffi::sqlite3_stmt>,
41    parameter_count: usize,
42    _connection: std::marker::PhantomData<&'connection ()>,
43}
44
45pub struct Rows<'statement, 'connection> {
46    statement: &'statement mut Statement<'connection>,
47    done: bool,
48    clear_bindings_on_drop: bool,
49}
50
51#[cfg(feature = "bench-profile")]
52#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
53pub struct QueryOptionalStringTextProfile {
54    pub reset_bind: u64,
55    pub step: u64,
56    pub column_read: u64,
57}
58
59#[cfg(feature = "bench-profile")]
60#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
61pub struct ExecuteTextTextProfile {
62    pub reset_bind: u64,
63    pub step: u64,
64}
65
66#[cfg(feature = "bench-profile")]
67#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
68pub struct QueryTextLenSumProfile {
69    pub reset_bind: u64,
70    pub row_scan: u64,
71}
72
73impl<'connection> Statement<'connection> {
74    pub(crate) fn new(db: *mut ffi::sqlite3, raw: NonNull<ffi::sqlite3_stmt>) -> Self {
75        let parameter_count =
76            usize::try_from(unsafe { ffi::sqlite3_bind_parameter_count(raw.as_ptr()) })
77                .unwrap_or(0);
78        Self::from_cached_raw(db, raw, parameter_count)
79    }
80
81    pub(crate) fn from_cached_raw(
82        db: *mut ffi::sqlite3,
83        raw: NonNull<ffi::sqlite3_stmt>,
84        parameter_count: usize,
85    ) -> Self {
86        Self {
87            db,
88            raw,
89            parameter_count,
90            _connection: std::marker::PhantomData,
91        }
92    }
93
94    pub(crate) fn parameter_count(&self) -> usize {
95        self.parameter_count
96    }
97
98    pub(crate) fn into_raw(self) -> NonNull<ffi::sqlite3_stmt> {
99        let raw = self.raw;
100        std::mem::forget(self);
101        raw
102    }
103
104    pub fn execute(&mut self, values: &[&dyn ToSql]) -> Result<(), DbError> {
105        self.reset_and_bind(values)?;
106        let rc = step(self.raw.as_ptr())?;
107        if rc == ffi::SQLITE_DONE {
108            Ok(())
109        } else {
110            Err(sqlite_error(self.db, rc))
111        }
112    }
113
114    pub fn execute_named(&mut self, values: &[(&str, &dyn ToSql)]) -> Result<(), DbError> {
115        self.reset_and_bind_named(values)?;
116        let rc = step(self.raw.as_ptr())?;
117        if rc == ffi::SQLITE_DONE {
118            Ok(())
119        } else {
120            Err(sqlite_error(self.db, rc))
121        }
122    }
123
124    /// Executes a two-TEXT statement without copying bound text into SQLite.
125    ///
126    /// The text is bound with `SQLITE_STATIC` only for the duration of the
127    /// SQLite step and cleared before this function returns.
128    #[inline(always)]
129    pub fn execute_text_text(&mut self, first: &str, second: &str) -> Result<(), DbError> {
130        if let Err(error) = self.reset_and_bind_two_text_borrowed(first, second) {
131            self.clear_bindings();
132            return Err(error);
133        }
134        let rc = step(self.raw.as_ptr());
135        let result = match rc {
136            Ok(ffi::SQLITE_DONE) => Ok(()),
137            Ok(rc) => Err(sqlite_error(self.db, rc)),
138            Err(error) => Err(error),
139        };
140        self.clear_bindings();
141        result
142    }
143
144    #[cfg(feature = "bench-profile")]
145    #[doc(hidden)]
146    pub fn execute_text_text_profiled(
147        &mut self,
148        first: &str,
149        second: &str,
150    ) -> Result<ExecuteTextTextProfile, DbError> {
151        let mut profile = ExecuteTextTextProfile::default();
152
153        let start = instruction_counter();
154        if let Err(error) = self.reset_and_bind_two_text_borrowed(first, second) {
155            self.clear_bindings();
156            return Err(error);
157        }
158        profile.reset_bind = instruction_counter().saturating_sub(start);
159
160        let start = instruction_counter();
161        let rc = step(self.raw.as_ptr());
162        profile.step = instruction_counter().saturating_sub(start);
163
164        let result = match rc {
165            Ok(ffi::SQLITE_DONE) => Ok(profile),
166            Ok(rc) => Err(sqlite_error(self.db, rc)),
167            Err(error) => Err(error),
168        };
169        self.clear_bindings();
170        result
171    }
172
173    /// Executes an INTEGER/TEXT statement without copying bound text into SQLite.
174    ///
175    /// The text is bound with `SQLITE_STATIC` only for the duration of the
176    /// SQLite step and cleared before this function returns.
177    #[inline(always)]
178    pub fn execute_i64_text(&mut self, first: i64, second: &str) -> Result<(), DbError> {
179        if let Err(error) = self.reset_and_bind_i64_text_borrowed(first, second) {
180            self.clear_bindings();
181            return Err(error);
182        }
183        let rc = step(self.raw.as_ptr());
184        let result = match rc {
185            Ok(ffi::SQLITE_DONE) => Ok(()),
186            Ok(rc) => Err(sqlite_error(self.db, rc)),
187            Err(error) => Err(error),
188        };
189        self.clear_bindings();
190        result
191    }
192
193    /// Executes an INTEGER/BLOB statement without copying bound bytes into SQLite.
194    ///
195    /// The blob is bound with `SQLITE_STATIC` only for the duration of the
196    /// SQLite step and cleared before this function returns.
197    #[inline(always)]
198    pub fn execute_i64_blob(&mut self, first: i64, second: &[u8]) -> Result<(), DbError> {
199        if let Err(error) = self.reset_and_bind_i64_blob_borrowed(first, second) {
200            self.clear_bindings();
201            return Err(error);
202        }
203        let rc = step(self.raw.as_ptr());
204        let result = match rc {
205            Ok(ffi::SQLITE_DONE) => Ok(()),
206            Ok(rc) => Err(sqlite_error(self.db, rc)),
207            Err(error) => Err(error),
208        };
209        self.clear_bindings();
210        result
211    }
212
213    /// Executes an INTEGER/INTEGER/TEXT statement without copying bound text into SQLite.
214    ///
215    /// The text is bound with `SQLITE_STATIC` only for the duration of the
216    /// SQLite step and cleared before this function returns.
217    #[inline(always)]
218    pub fn execute_i64_i64_text(
219        &mut self,
220        first: i64,
221        second: i64,
222        third: &str,
223    ) -> Result<(), DbError> {
224        if let Err(error) = self.reset_and_bind_i64_i64_text_borrowed(first, second, third) {
225            self.clear_bindings();
226            return Err(error);
227        }
228        let rc = step(self.raw.as_ptr());
229        let result = match rc {
230            Ok(ffi::SQLITE_DONE) => Ok(()),
231            Ok(rc) => Err(sqlite_error(self.db, rc)),
232            Err(error) => Err(error),
233        };
234        self.clear_bindings();
235        result
236    }
237
238    pub fn query<'statement>(
239        &'statement mut self,
240        values: &[&dyn ToSql],
241    ) -> Result<Rows<'statement, 'connection>, DbError> {
242        self.reset_and_bind(values)?;
243        Ok(Rows {
244            statement: self,
245            done: false,
246            clear_bindings_on_drop: false,
247        })
248    }
249
250    #[doc(hidden)]
251    /// Runs a query with one INTEGER parameter without dynamic parameter dispatch.
252    #[inline(always)]
253    pub fn query_i64<'statement>(
254        &'statement mut self,
255        value: i64,
256    ) -> Result<Rows<'statement, 'connection>, DbError> {
257        self.reset_and_bind_single_i64(value)?;
258        Ok(Rows {
259            statement: self,
260            done: false,
261            clear_bindings_on_drop: false,
262        })
263    }
264
265    #[doc(hidden)]
266    /// Runs a query with borrowed TEXT parameters without dynamic parameter dispatch.
267    pub fn query_texts<'statement>(
268        &'statement mut self,
269        values: &'statement [&'statement str],
270    ) -> Result<Rows<'statement, 'connection>, DbError> {
271        self.reset_and_bind_text_iter(values.iter().copied())?;
272        Ok(Rows {
273            statement: self,
274            done: false,
275            clear_bindings_on_drop: true,
276        })
277    }
278
279    #[doc(hidden)]
280    /// Runs a query with borrowed TEXT parameters without materializing a slice.
281    pub fn query_text_iter<'statement, I>(
282        &'statement mut self,
283        values: I,
284    ) -> Result<Rows<'statement, 'connection>, DbError>
285    where
286        I: ExactSizeIterator<Item = &'statement str>,
287    {
288        self.reset_and_bind_text_iter(values)?;
289        Ok(Rows {
290            statement: self,
291            done: false,
292            clear_bindings_on_drop: true,
293        })
294    }
295
296    #[doc(hidden)]
297    /// Runs a one-shot query with borrowed TEXT parameters.
298    ///
299    /// Borrowed bindings are cleared when the returned rows are dropped.
300    pub fn query_text_iter_ephemeral<'statement, I>(
301        &'statement mut self,
302        values: I,
303    ) -> Result<Rows<'statement, 'connection>, DbError>
304    where
305        I: ExactSizeIterator<Item = &'statement str>,
306    {
307        self.reset_and_bind_text_iter(values)?;
308        Ok(Rows {
309            statement: self,
310            done: false,
311            clear_bindings_on_drop: true,
312        })
313    }
314
315    #[doc(hidden)]
316    /// Runs a borrowed-TEXT query and sums column 0 byte lengths.
317    ///
318    /// This keeps cached borrowed bindings safe by clearing them before return.
319    pub fn query_text_iter_text_len_sum<'value, I>(&mut self, values: I) -> Result<u64, DbError>
320    where
321        I: ExactSizeIterator<Item = &'value str>,
322    {
323        if let Err(error) = self.reset_and_bind_text_iter(values) {
324            self.clear_bindings();
325            return Err(error);
326        }
327
328        let statement = self.raw.as_ptr();
329        let mut total = 0_u64;
330        loop {
331            match step(statement) {
332                Ok(ffi::SQLITE_ROW) => {
333                    total = total.wrapping_add(read_string_column_zero_len(statement) as u64);
334                }
335                Ok(ffi::SQLITE_DONE) => {
336                    self.clear_bindings();
337                    return Ok(total);
338                }
339                Ok(rc) => {
340                    self.clear_bindings();
341                    return Err(sqlite_error(self.db, rc));
342                }
343                Err(error) => {
344                    self.clear_bindings();
345                    return Err(error);
346                }
347            }
348        }
349    }
350
351    #[cfg(feature = "bench-profile")]
352    #[doc(hidden)]
353    pub fn query_text_iter_text_len_sum_profiled<'value, I>(
354        &mut self,
355        values: I,
356    ) -> Result<(u64, QueryTextLenSumProfile), DbError>
357    where
358        I: ExactSizeIterator<Item = &'value str>,
359    {
360        let mut profile = QueryTextLenSumProfile::default();
361
362        let start = instruction_counter();
363        if let Err(error) = self.reset_and_bind_text_iter(values) {
364            self.clear_bindings();
365            return Err(error);
366        }
367        profile.reset_bind = instruction_counter().saturating_sub(start);
368
369        let start = instruction_counter();
370        let statement = self.raw.as_ptr();
371        let mut total = 0_u64;
372        loop {
373            match step(statement) {
374                Ok(ffi::SQLITE_ROW) => {
375                    total = total.wrapping_add(read_string_column_zero_len(statement) as u64);
376                }
377                Ok(ffi::SQLITE_DONE) => {
378                    profile.row_scan = instruction_counter().saturating_sub(start);
379                    self.clear_bindings();
380                    return Ok((total, profile));
381                }
382                Ok(rc) => {
383                    self.clear_bindings();
384                    return Err(sqlite_error(self.db, rc));
385                }
386                Err(error) => {
387                    self.clear_bindings();
388                    return Err(error);
389                }
390            }
391        }
392    }
393
394    pub fn query_named<'statement>(
395        &'statement mut self,
396        values: &[(&str, &dyn ToSql)],
397    ) -> Result<Rows<'statement, 'connection>, DbError> {
398        self.reset_and_bind_named(values)?;
399        Ok(Rows {
400            statement: self,
401            done: false,
402            clear_bindings_on_drop: false,
403        })
404    }
405
406    pub fn query_one<T, F>(&mut self, values: &[&dyn ToSql], f: F) -> Result<T, DbError>
407    where
408        F: FnOnce(&Row<'_>) -> Result<T, DbError>,
409    {
410        let mut rows = self.query(values)?;
411        match rows.next_row()? {
412            Some(row) => f(&row),
413            None => Err(DbError::NotFound),
414        }
415    }
416
417    pub fn query_one_named<T, F>(
418        &mut self,
419        values: &[(&str, &dyn ToSql)],
420        f: F,
421    ) -> Result<T, DbError>
422    where
423        F: FnOnce(&Row<'_>) -> Result<T, DbError>,
424    {
425        let mut rows = self.query_named(values)?;
426        match rows.next_row()? {
427            Some(row) => f(&row),
428            None => Err(DbError::NotFound),
429        }
430    }
431
432    pub fn query_optional<T, F>(
433        &mut self,
434        values: &[&dyn ToSql],
435        f: F,
436    ) -> Result<Option<T>, DbError>
437    where
438        F: FnOnce(&Row<'_>) -> Result<T, DbError>,
439    {
440        let mut rows = self.query(values)?;
441        match rows.next_row()? {
442            Some(row) => f(&row).map(Some),
443            None => Ok(None),
444        }
445    }
446
447    pub fn query_optional_named<T, F>(
448        &mut self,
449        values: &[(&str, &dyn ToSql)],
450        f: F,
451    ) -> Result<Option<T>, DbError>
452    where
453        F: FnOnce(&Row<'_>) -> Result<T, DbError>,
454    {
455        let mut rows = self.query_named(values)?;
456        match rows.next_row()? {
457            Some(row) => f(&row).map(Some),
458            None => Ok(None),
459        }
460    }
461
462    pub fn query_all<T, F>(&mut self, values: &[&dyn ToSql], mut f: F) -> Result<Vec<T>, DbError>
463    where
464        F: FnMut(&Row<'_>) -> Result<T, DbError>,
465    {
466        let mut rows = self.query(values)?;
467        let mut output = Vec::new();
468        while let Some(row) = rows.next_row()? {
469            output.push(f(&row)?);
470        }
471        Ok(output)
472    }
473
474    pub fn query_all_named<T, F>(
475        &mut self,
476        values: &[(&str, &dyn ToSql)],
477        mut f: F,
478    ) -> Result<Vec<T>, DbError>
479    where
480        F: FnMut(&Row<'_>) -> Result<T, DbError>,
481    {
482        let mut rows = self.query_named(values)?;
483        let mut output = Vec::new();
484        while let Some(row) = rows.next_row()? {
485            output.push(f(&row)?);
486        }
487        Ok(output)
488    }
489
490    pub fn query_scalar<T: FromColumn>(&mut self, values: &[&dyn ToSql]) -> Result<T, DbError> {
491        self.query_one(values, |row| row.get(0))
492    }
493    pub fn query_scalar_named<T: FromColumn>(
494        &mut self,
495        values: &[(&str, &dyn ToSql)],
496    ) -> Result<T, DbError> {
497        self.query_one_named(values, |row| row.get(0))
498    }
499
500    pub fn query_optional_scalar<T: FromColumn>(
501        &mut self,
502        values: &[&dyn ToSql],
503    ) -> Result<Option<T>, DbError> {
504        self.query_optional(values, |row| row.get(0))
505    }
506
507    #[inline(always)]
508    pub fn query_optional_string_text(&mut self, value: &str) -> Result<Option<String>, DbError> {
509        self.query_optional_string_text_borrowed(value)
510    }
511
512    #[inline(always)]
513    pub(crate) fn query_optional_string_text_borrowed(
514        &mut self,
515        value: &str,
516    ) -> Result<Option<String>, DbError> {
517        if let Err(error) = self.reset_and_bind_single_text_borrowed(value) {
518            self.clear_bindings();
519            return Err(error);
520        }
521        let rc = step(self.raw.as_ptr());
522        let result = match rc {
523            Ok(ffi::SQLITE_ROW) => read_string_column_zero(self.raw.as_ptr()).map(Some),
524            Ok(ffi::SQLITE_DONE) => Ok(None),
525            Ok(rc) => Err(sqlite_error(self.db, rc)),
526            Err(error) => Err(error),
527        };
528        self.clear_bindings();
529        result
530    }
531
532    #[inline(always)]
533    pub fn query_optional_string_text_len(
534        &mut self,
535        value: &str,
536    ) -> Result<Option<usize>, DbError> {
537        self.query_optional_string_text_len_borrowed(value)
538    }
539
540    #[inline(always)]
541    pub(crate) fn query_optional_string_text_len_borrowed(
542        &mut self,
543        value: &str,
544    ) -> Result<Option<usize>, DbError> {
545        if let Err(error) = self.reset_and_bind_single_text_borrowed(value) {
546            self.clear_bindings();
547            return Err(error);
548        }
549        let rc = step(self.raw.as_ptr());
550        let result = match rc {
551            Ok(ffi::SQLITE_ROW) => Ok(Some(read_string_column_zero_len(self.raw.as_ptr()))),
552            Ok(ffi::SQLITE_DONE) => Ok(None),
553            Ok(rc) => Err(sqlite_error(self.db, rc)),
554            Err(error) => Err(error),
555        };
556        self.clear_bindings();
557        result
558    }
559
560    #[cfg(feature = "bench-profile")]
561    #[doc(hidden)]
562    pub fn query_optional_string_text_profiled(
563        &mut self,
564        value: &str,
565    ) -> Result<(Option<String>, QueryOptionalStringTextProfile), DbError> {
566        let mut profile = QueryOptionalStringTextProfile::default();
567
568        let start = instruction_counter();
569        if let Err(error) = self.reset_and_bind_single_text_borrowed(value) {
570            self.clear_bindings();
571            return Err(error);
572        }
573        profile.reset_bind = instruction_counter().saturating_sub(start);
574
575        let start = instruction_counter();
576        let rc = step(self.raw.as_ptr());
577        profile.step = instruction_counter().saturating_sub(start);
578
579        match rc {
580            Ok(ffi::SQLITE_ROW) => {
581                let start = instruction_counter();
582                let value = read_string_column_zero(self.raw.as_ptr()).map(Some);
583                profile.column_read = instruction_counter().saturating_sub(start);
584                self.clear_bindings();
585                value.map(|value| (value, profile))
586            }
587            Ok(ffi::SQLITE_DONE) => {
588                self.clear_bindings();
589                Ok((None, profile))
590            }
591            Ok(rc) => {
592                self.clear_bindings();
593                Err(sqlite_error(self.db, rc))
594            }
595            Err(error) => {
596                self.clear_bindings();
597                Err(error)
598            }
599        }
600    }
601
602    #[cfg(feature = "bench-profile")]
603    #[doc(hidden)]
604    pub fn query_optional_string_text_len_profiled(
605        &mut self,
606        value: &str,
607    ) -> Result<(Option<usize>, QueryOptionalStringTextProfile), DbError> {
608        let mut profile = QueryOptionalStringTextProfile::default();
609
610        let start = instruction_counter();
611        if let Err(error) = self.reset_and_bind_single_text_borrowed(value) {
612            self.clear_bindings();
613            return Err(error);
614        }
615        profile.reset_bind = instruction_counter().saturating_sub(start);
616
617        let start = instruction_counter();
618        let rc = step(self.raw.as_ptr());
619        profile.step = instruction_counter().saturating_sub(start);
620
621        match rc {
622            Ok(ffi::SQLITE_ROW) => {
623                let start = instruction_counter();
624                let value = Some(read_string_column_zero_len(self.raw.as_ptr()));
625                profile.column_read = instruction_counter().saturating_sub(start);
626                self.clear_bindings();
627                Ok((value, profile))
628            }
629            Ok(ffi::SQLITE_DONE) => {
630                self.clear_bindings();
631                Ok((None, profile))
632            }
633            Ok(rc) => {
634                self.clear_bindings();
635                Err(sqlite_error(self.db, rc))
636            }
637            Err(error) => {
638                self.clear_bindings();
639                Err(error)
640            }
641        }
642    }
643
644    pub fn query_optional_scalar_named<T: FromColumn>(
645        &mut self,
646        values: &[(&str, &dyn ToSql)],
647    ) -> Result<Option<T>, DbError> {
648        self.query_optional_named(values, |row| row.get(0))
649    }
650
651    pub fn query_column<T: FromColumn>(
652        &mut self,
653        values: &[&dyn ToSql],
654    ) -> Result<Vec<T>, DbError> {
655        self.query_all(values, |row| row.get(0))
656    }
657
658    pub fn query_column_named<T: FromColumn>(
659        &mut self,
660        values: &[(&str, &dyn ToSql)],
661    ) -> Result<Vec<T>, DbError> {
662        self.query_all_named(values, |row| row.get(0))
663    }
664
665    fn reset_and_bind(&mut self, values: &[&dyn ToSql]) -> Result<(), DbError> {
666        let reset_rc = unsafe { ffi::sqlite3_reset(self.raw.as_ptr()) };
667        if reset_rc != ffi::SQLITE_OK {
668            return Err(sqlite_error(self.db, reset_rc));
669        }
670        if self.parameter_count == 0 && values.is_empty() {
671            return Ok(());
672        }
673        bind_all_with_count(self.raw.as_ptr(), values, self.parameter_count)
674    }
675
676    #[inline(always)]
677    fn reset_and_bind_single_text_borrowed(&mut self, value: &str) -> Result<(), DbError> {
678        if self.parameter_count != 1 {
679            return Err(DbError::ParameterCountMismatch {
680                expected: self.parameter_count,
681                actual: 1,
682            });
683        }
684        let reset_rc = unsafe { ffi::sqlite3_reset(self.raw.as_ptr()) };
685        if reset_rc != ffi::SQLITE_OK {
686            return Err(sqlite_error(self.db, reset_rc));
687        }
688        bind_text_static(self.raw.as_ptr(), 1, value)
689    }
690
691    #[inline(always)]
692    fn reset_and_bind_single_i64(&mut self, value: i64) -> Result<(), DbError> {
693        if self.parameter_count != 1 {
694            return Err(DbError::ParameterCountMismatch {
695                expected: self.parameter_count,
696                actual: 1,
697            });
698        }
699        let reset_rc = unsafe { ffi::sqlite3_reset(self.raw.as_ptr()) };
700        if reset_rc != ffi::SQLITE_OK {
701            return Err(sqlite_error(self.db, reset_rc));
702        }
703        bind_i64(self.raw.as_ptr(), 1, value)
704    }
705
706    #[inline(always)]
707    fn reset_and_bind_two_text_borrowed(
708        &mut self,
709        first: &str,
710        second: &str,
711    ) -> Result<(), DbError> {
712        if self.parameter_count != 2 {
713            return Err(DbError::ParameterCountMismatch {
714                expected: self.parameter_count,
715                actual: 2,
716            });
717        }
718        let reset_rc = unsafe { ffi::sqlite3_reset(self.raw.as_ptr()) };
719        if reset_rc != ffi::SQLITE_OK {
720            return Err(sqlite_error(self.db, reset_rc));
721        }
722        let first_len =
723            std::ffi::c_int::try_from(first.len()).map_err(|_| DbError::TextTooLarge)?;
724        let first_rc = unsafe {
725            ffi::sqlite3_bind_text(
726                self.raw.as_ptr(),
727                1,
728                first.as_ptr().cast(),
729                first_len,
730                ffi::SQLITE_STATIC(),
731            )
732        };
733        if first_rc != ffi::SQLITE_OK {
734            return Err(DbError::Sqlite(first_rc, "sqlite bind failed".to_string()));
735        }
736        let second_len =
737            std::ffi::c_int::try_from(second.len()).map_err(|_| DbError::TextTooLarge)?;
738        let second_rc = unsafe {
739            ffi::sqlite3_bind_text(
740                self.raw.as_ptr(),
741                2,
742                second.as_ptr().cast(),
743                second_len,
744                ffi::SQLITE_STATIC(),
745            )
746        };
747        if second_rc == ffi::SQLITE_OK {
748            Ok(())
749        } else {
750            Err(DbError::Sqlite(second_rc, "sqlite bind failed".to_string()))
751        }
752    }
753
754    fn reset_and_bind_text_iter<'value, I>(&mut self, values: I) -> Result<(), DbError>
755    where
756        I: ExactSizeIterator<Item = &'value str>,
757    {
758        let actual = values.len();
759        if actual != self.parameter_count {
760            return Err(DbError::ParameterCountMismatch {
761                expected: self.parameter_count,
762                actual,
763            });
764        }
765        let reset_rc = unsafe { ffi::sqlite3_reset(self.raw.as_ptr()) };
766        if reset_rc != ffi::SQLITE_OK {
767            return Err(sqlite_error(self.db, reset_rc));
768        }
769        let mut param = 1;
770        for value in values {
771            let len = match std::ffi::c_int::try_from(value.len()) {
772                Ok(len) => len,
773                Err(_) => {
774                    self.clear_bindings();
775                    return Err(DbError::TextTooLarge);
776                }
777            };
778            let rc = unsafe {
779                ffi::sqlite3_bind_text(
780                    self.raw.as_ptr(),
781                    param,
782                    value.as_ptr().cast(),
783                    len,
784                    ffi::SQLITE_STATIC(),
785                )
786            };
787            if rc != ffi::SQLITE_OK {
788                self.clear_bindings();
789                return Err(DbError::Sqlite(rc, "sqlite bind failed".to_string()));
790            }
791            param += 1;
792        }
793        Ok(())
794    }
795
796    #[inline(always)]
797    fn reset_and_bind_i64_text_borrowed(
798        &mut self,
799        first: i64,
800        second: &str,
801    ) -> Result<(), DbError> {
802        if self.parameter_count != 2 {
803            return Err(DbError::ParameterCountMismatch {
804                expected: self.parameter_count,
805                actual: 2,
806            });
807        }
808        let reset_rc = unsafe { ffi::sqlite3_reset(self.raw.as_ptr()) };
809        if reset_rc != ffi::SQLITE_OK {
810            return Err(sqlite_error(self.db, reset_rc));
811        }
812        bind_i64(self.raw.as_ptr(), 1, first)?;
813        let second_len =
814            std::ffi::c_int::try_from(second.len()).map_err(|_| DbError::TextTooLarge)?;
815        let second_rc = unsafe {
816            ffi::sqlite3_bind_text(
817                self.raw.as_ptr(),
818                2,
819                second.as_ptr().cast(),
820                second_len,
821                ffi::SQLITE_STATIC(),
822            )
823        };
824        if second_rc == ffi::SQLITE_OK {
825            Ok(())
826        } else {
827            Err(DbError::Sqlite(second_rc, "sqlite bind failed".to_string()))
828        }
829    }
830
831    #[inline(always)]
832    fn reset_and_bind_i64_blob_borrowed(
833        &mut self,
834        first: i64,
835        second: &[u8],
836    ) -> Result<(), DbError> {
837        if self.parameter_count != 2 {
838            return Err(DbError::ParameterCountMismatch {
839                expected: self.parameter_count,
840                actual: 2,
841            });
842        }
843        let reset_rc = unsafe { ffi::sqlite3_reset(self.raw.as_ptr()) };
844        if reset_rc != ffi::SQLITE_OK {
845            return Err(sqlite_error(self.db, reset_rc));
846        }
847        bind_i64(self.raw.as_ptr(), 1, first)?;
848        bind_blob_static(self.raw.as_ptr(), 2, second)
849    }
850
851    #[inline(always)]
852    fn reset_and_bind_i64_i64_text_borrowed(
853        &mut self,
854        first: i64,
855        second: i64,
856        third: &str,
857    ) -> Result<(), DbError> {
858        if self.parameter_count != 3 {
859            return Err(DbError::ParameterCountMismatch {
860                expected: self.parameter_count,
861                actual: 3,
862            });
863        }
864        let reset_rc = unsafe { ffi::sqlite3_reset(self.raw.as_ptr()) };
865        if reset_rc != ffi::SQLITE_OK {
866            return Err(sqlite_error(self.db, reset_rc));
867        }
868        bind_i64(self.raw.as_ptr(), 1, first)?;
869        bind_i64(self.raw.as_ptr(), 2, second)?;
870        let third_len =
871            std::ffi::c_int::try_from(third.len()).map_err(|_| DbError::TextTooLarge)?;
872        let third_rc = unsafe {
873            ffi::sqlite3_bind_text(
874                self.raw.as_ptr(),
875                3,
876                third.as_ptr().cast(),
877                third_len,
878                ffi::SQLITE_STATIC(),
879            )
880        };
881        if third_rc == ffi::SQLITE_OK {
882            Ok(())
883        } else {
884            Err(DbError::Sqlite(third_rc, "sqlite bind failed".to_string()))
885        }
886    }
887
888    fn clear_bindings(&mut self) {
889        unsafe {
890            ffi::sqlite3_clear_bindings(self.raw.as_ptr());
891        }
892    }
893
894    fn reset_and_bind_named(&mut self, values: &[(&str, &dyn ToSql)]) -> Result<(), DbError> {
895        let reset_rc = unsafe { ffi::sqlite3_reset(self.raw.as_ptr()) };
896        if reset_rc != ffi::SQLITE_OK {
897            return Err(sqlite_error(self.db, reset_rc));
898        }
899        bind_named_all(self.raw.as_ptr(), values)
900    }
901}
902
903static EMPTY_BLOB: u8 = 0;
904
905#[inline(always)]
906fn read_string_column_zero(statement: *mut ffi::sqlite3_stmt) -> Result<String, DbError> {
907    let actual = unsafe { ffi::sqlite3_column_type(statement, 0) };
908    if actual != ffi::SQLITE_TEXT {
909        return Err(DbError::TypeMismatch {
910            index: 0,
911            expected: "TEXT",
912            actual: sqlite_type_name(actual),
913        });
914    }
915    let text = unsafe { ffi::sqlite3_column_text(statement, 0) };
916    let len = unsafe { ffi::sqlite3_column_bytes(statement, 0) as usize };
917    if len == 0 || text.is_null() {
918        return Ok(String::new());
919    }
920    let bytes = unsafe { std::slice::from_raw_parts(text.cast::<u8>(), len) };
921    match std::str::from_utf8(bytes) {
922        Ok(value) => Ok(value.to_owned()),
923        Err(_) => Ok(String::from_utf8_lossy(bytes).into_owned()),
924    }
925}
926
927#[inline(always)]
928fn read_string_column_zero_len(statement: *mut ffi::sqlite3_stmt) -> usize {
929    unsafe { ffi::sqlite3_column_bytes(statement, 0) as usize }
930}
931
932#[inline]
933fn bind_text_static(
934    statement: *mut ffi::sqlite3_stmt,
935    index: std::ffi::c_int,
936    value: &str,
937) -> Result<(), DbError> {
938    let len = std::ffi::c_int::try_from(value.len()).map_err(|_| DbError::TextTooLarge)?;
939    let rc = unsafe {
940        ffi::sqlite3_bind_text(
941            statement,
942            index,
943            value.as_ptr().cast(),
944            len,
945            ffi::SQLITE_STATIC(),
946        )
947    };
948    if rc == ffi::SQLITE_OK {
949        Ok(())
950    } else {
951        Err(DbError::Sqlite(rc, "sqlite bind failed".to_string()))
952    }
953}
954
955#[inline(always)]
956fn bind_blob_static(
957    statement: *mut ffi::sqlite3_stmt,
958    index: std::ffi::c_int,
959    value: &[u8],
960) -> Result<(), DbError> {
961    let len = std::ffi::c_int::try_from(value.len()).map_err(|_| DbError::BlobTooLarge)?;
962    let ptr = if value.is_empty() {
963        (&EMPTY_BLOB as *const u8).cast::<c_void>()
964    } else {
965        value.as_ptr().cast::<c_void>()
966    };
967    let rc = unsafe { ffi::sqlite3_bind_blob(statement, index, ptr, len, ffi::SQLITE_STATIC()) };
968    if rc == ffi::SQLITE_OK {
969        Ok(())
970    } else {
971        Err(DbError::Sqlite(rc, "sqlite bind failed".to_string()))
972    }
973}
974
975#[inline(always)]
976fn bind_i64(
977    statement: *mut ffi::sqlite3_stmt,
978    index: std::ffi::c_int,
979    value: i64,
980) -> Result<(), DbError> {
981    let rc = unsafe { ffi::sqlite3_bind_int64(statement, index, value) };
982    if rc == ffi::SQLITE_OK {
983        Ok(())
984    } else {
985        Err(DbError::Sqlite(rc, "sqlite bind failed".to_string()))
986    }
987}
988
989fn sqlite_type_name(code: std::ffi::c_int) -> &'static str {
990    match code {
991        ffi::SQLITE_INTEGER => "INTEGER",
992        ffi::SQLITE_FLOAT => "REAL",
993        ffi::SQLITE_TEXT => "TEXT",
994        ffi::SQLITE_BLOB => "BLOB",
995        ffi::SQLITE_NULL => "NULL",
996        _ => "UNKNOWN",
997    }
998}
999
1000impl Rows<'_, '_> {
1001    pub fn next_row(&mut self) -> Result<Option<Row<'_>>, DbError> {
1002        if self.done {
1003            return Ok(None);
1004        }
1005        let rc = step(self.statement.raw.as_ptr())?;
1006        match rc {
1007            ffi::SQLITE_ROW => Ok(Some(Row::new(self.statement.raw.as_ptr()))),
1008            ffi::SQLITE_DONE => {
1009                self.done = true;
1010                self.clear_static_bindings();
1011                Ok(None)
1012            }
1013            _ => Err(sqlite_error(self.statement.db, rc)),
1014        }
1015    }
1016
1017    #[doc(hidden)]
1018    /// Reads column 0 byte length for call sites that already select a known TEXT expression.
1019    ///
1020    /// General typed reads still use `Row::get`; this helper skips per-row type
1021    /// checks for hot benchmark paths that only need TEXT lengths.
1022    #[inline(always)]
1023    pub fn next_text_len_zero(&mut self) -> Result<Option<usize>, DbError> {
1024        let statement = self.statement.raw.as_ptr();
1025        let rc = step(statement)?;
1026        match rc {
1027            ffi::SQLITE_ROW => Ok(Some(read_string_column_zero_len(statement))),
1028            ffi::SQLITE_DONE => {
1029                self.done = true;
1030                self.clear_static_bindings();
1031                Ok(None)
1032            }
1033            _ => Err(sqlite_error(self.statement.db, rc)),
1034        }
1035    }
1036
1037    fn clear_static_bindings(&mut self) {
1038        if self.clear_bindings_on_drop {
1039            self.statement.clear_bindings();
1040            self.clear_bindings_on_drop = false;
1041        }
1042    }
1043}
1044
1045impl Drop for Rows<'_, '_> {
1046    fn drop(&mut self) {
1047        self.clear_static_bindings();
1048    }
1049}
1050
1051#[inline(always)]
1052fn step(statement: *mut ffi::sqlite3_stmt) -> Result<std::ffi::c_int, DbError> {
1053    #[cfg(any(test, feature = "canister-api-test-failpoints"))]
1054    if let Some(code) = hit_step_failpoint() {
1055        return Err(DbError::Sqlite(code, "sqlite step failpoint".to_string()));
1056    }
1057    Ok(unsafe { ffi::sqlite3_step(statement) })
1058}
1059
1060#[cfg(feature = "bench-profile")]
1061fn instruction_counter() -> u64 {
1062    #[cfg(target_arch = "wasm32")]
1063    {
1064        ic_cdk::api::performance_counter(0)
1065    }
1066    #[cfg(not(target_arch = "wasm32"))]
1067    {
1068        0
1069    }
1070}
1071
1072#[cfg(any(test, feature = "canister-api-test-failpoints"))]
1073pub fn set_step_failpoint(failpoint: StepFailpoint) {
1074    if let Ok(context) = crate::stable::memory::active_context_id() {
1075        STEP_FAILPOINTS.with(|slot| {
1076            slot.borrow_mut().insert(
1077                context,
1078                StepFailpointState {
1079                    failpoint,
1080                    count: 0,
1081                },
1082            );
1083        });
1084    }
1085}
1086
1087#[cfg(any(test, feature = "canister-api-test-failpoints"))]
1088pub fn clear_step_failpoint() {
1089    STEP_FAILPOINTS.with(|slot| slot.borrow_mut().clear());
1090}
1091
1092#[cfg(any(test, feature = "canister-api-test-failpoints"))]
1093fn hit_step_failpoint() -> Option<std::ffi::c_int> {
1094    let Ok(context) = crate::stable::memory::active_context_id() else {
1095        return None;
1096    };
1097    STEP_FAILPOINTS.with(|slot| {
1098        let mut slot = slot.borrow_mut();
1099        let state = slot.get_mut(&context)?;
1100        state.count += 1;
1101        if state.failpoint.ordinal == state.count {
1102            let code = state.failpoint.code;
1103            slot.remove(&context);
1104            Some(code)
1105        } else {
1106            None
1107        }
1108    })
1109}
1110
1111impl Drop for Statement<'_> {
1112    fn drop(&mut self) {
1113        unsafe {
1114            ffi::sqlite3_finalize(self.raw.as_ptr());
1115        }
1116    }
1117}