Skip to main content

diesel/sqlite/connection/
pragmas.rs

1use super::SqliteConnection;
2use crate::query_builder::{AstPass, Query, QueryFragment, QueryId};
3use crate::query_dsl::RunQueryDslSupport;
4use crate::result::QueryResult;
5use crate::sqlite::Sqlite;
6use alloc::string::ToString;
7use core::marker::PhantomData;
8
9/// The `auto_vacuum` mode of a database, controlling whether and when SQLite
10/// reclaims freed pages back to the file.
11///
12/// The mode is stored in the database file, not the connection. [`Full`] and
13/// [`Incremental`] can be switched between at any time, but changing from or to
14/// [`None`] only takes effect on a database with no tables yet, or after a
15/// subsequent `VACUUM` rewrites the file.
16///
17/// [`None`]: AutoVacuumMode::None
18/// [`Full`]: AutoVacuumMode::Full
19/// [`Incremental`]: AutoVacuumMode::Incremental
20#[derive(#[automatically_derived]
impl ::core::fmt::Debug for AutoVacuumMode {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::write_str(f,
            match self {
                AutoVacuumMode::None => "None",
                AutoVacuumMode::Full => "Full",
                AutoVacuumMode::Incremental => "Incremental",
            })
    }
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for AutoVacuumMode { }
#[automatically_derived]
impl ::core::clone::Clone for AutoVacuumMode {
    #[inline]
    fn clone(&self) -> Self { *self }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for AutoVacuumMode { }Copy, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for AutoVacuumMode { }
#[automatically_derived]
impl ::core::cmp::PartialEq for AutoVacuumMode {
    #[inline]
    fn eq(&self, other: &Self) -> bool {
        ::core::intrinsics::discriminant_value(self) ==
            ::core::intrinsics::discriminant_value(other)
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for AutoVacuumMode { }Eq, const _: () =
    {
        use diesel;
        impl<__DB>
            diesel::deserialize::FromSql<crate::sql_types::Integer, __DB> for
            AutoVacuumMode where __DB: diesel::backend::Backend,
            crate::sql_types::Integer: diesel::sql_types::EnumSqlType<true,
            __DB>,
            <crate::sql_types::Integer as
            diesel::sql_types::EnumSqlType<true,
            __DB>>::Strategy: diesel::internal::derives::enum_::EnumMapping<__DB>
            {
            fn from_sql(value:
                    <__DB as diesel::backend::Backend>::RawValue<'_>)
                -> diesel::deserialize::Result<Self> {
                const VARIANTS:
                    &[diesel::internal::derives::enum_::EnumVariant] =
                    &[diesel::internal::derives::enum_::EnumVariant {
                                    discriminant: 0i128,
                                    rust_name: "None",
                                    sql_name: "None",
                                },
                                diesel::internal::derives::enum_::EnumVariant {
                                    discriminant: 1i128,
                                    rust_name: "Full",
                                    sql_name: "Full",
                                },
                                diesel::internal::derives::enum_::EnumVariant {
                                    discriminant: 2i128,
                                    rust_name: "Incremental",
                                    sql_name: "Incremental",
                                }];
                let idx =
                    <<crate::sql_types::Integer as
                                diesel::sql_types::EnumSqlType<true, __DB>>::Strategy as
                                diesel::internal::derives::enum_::EnumMapping<__DB>>::map_from_database_value(value,
                            "AutoVacuumMode", VARIANTS)?;
                match idx {
                    0usize => Ok(Self::None),
                    1usize => Ok(Self::Full),
                    2usize => Ok(Self::Incremental),
                    _ => {
                        ::core::panicking::panic_fmt(format_args!("internal error: entered unreachable code: {0}",
                                format_args!("We construct all relevant variants")));
                    }
                }
            }
        }
        impl<__DB> diesel::serialize::ToSql<crate::sql_types::Integer, __DB>
            for AutoVacuumMode where __DB: diesel::backend::Backend,
            crate::sql_types::Integer: diesel::sql_types::EnumSqlType<true,
            __DB>,
            <crate::sql_types::Integer as
            diesel::sql_types::EnumSqlType<true,
            __DB>>::Strategy: diesel::internal::derives::enum_::EnumMapping<__DB>
            {
            fn to_sql<'b>(&'b self,
                output: &mut diesel::serialize::Output<'b, '_, __DB>)
                -> diesel::serialize::Result {
                let variant =
                    match self {
                        Self::None =>
                            &diesel::internal::derives::enum_::EnumVariant {
                                    discriminant: 0i128,
                                    rust_name: "None",
                                    sql_name: "None",
                                },
                        Self::Full =>
                            &diesel::internal::derives::enum_::EnumVariant {
                                    discriminant: 1i128,
                                    rust_name: "Full",
                                    sql_name: "Full",
                                },
                        Self::Incremental =>
                            &diesel::internal::derives::enum_::EnumVariant {
                                    discriminant: 2i128,
                                    rust_name: "Incremental",
                                    sql_name: "Incremental",
                                },
                    };
                <<crate::sql_types::Integer as
                        diesel::sql_types::EnumSqlType<true, __DB>>::Strategy as
                        diesel::internal::derives::enum_::EnumMapping<__DB>>::map_to_database_value(output,
                    variant)
            }
        }
        impl<'__expr>
            diesel::expression::AsExpression<crate::sql_types::Integer> for
            &'__expr AutoVacuumMode {
            type Expression =
                diesel::internal::derives::as_expression::Bound<crate::sql_types::Integer,
                Self>;
            fn as_expression(self)
                ->
                    <Self as
                    diesel::expression::AsExpression<crate::sql_types::Integer>>::Expression {
                diesel::internal::derives::as_expression::Bound::new(self)
            }
        }
        #[diagnostic::do_not_recommend]
        impl<'__expr>
            diesel::expression::AsExpression<diesel::sql_types::Nullable<crate::sql_types::Integer>>
            for &'__expr AutoVacuumMode {
            type Expression =
                diesel::internal::derives::as_expression::Bound<diesel::sql_types::Nullable<crate::sql_types::Integer>,
                Self>;
            fn as_expression(self)
                ->
                    <Self as
                    diesel::expression::AsExpression<diesel::sql_types::Nullable<crate::sql_types::Integer>>>::Expression {
                diesel::internal::derives::as_expression::Bound::new(self)
            }
        }
        #[diagnostic::do_not_recommend]
        impl<'__expr, '__expr2>
            diesel::expression::AsExpression<crate::sql_types::Integer> for
            &'__expr2 &'__expr AutoVacuumMode {
            type Expression =
                diesel::internal::derives::as_expression::Bound<crate::sql_types::Integer,
                Self>;
            fn as_expression(self)
                ->
                    <Self as
                    diesel::expression::AsExpression<crate::sql_types::Integer>>::Expression {
                diesel::internal::derives::as_expression::Bound::new(self)
            }
        }
        #[diagnostic::do_not_recommend]
        impl<'__expr, '__expr2>
            diesel::expression::AsExpression<diesel::sql_types::Nullable<crate::sql_types::Integer>>
            for &'__expr2 &'__expr AutoVacuumMode {
            type Expression =
                diesel::internal::derives::as_expression::Bound<diesel::sql_types::Nullable<crate::sql_types::Integer>,
                Self>;
            fn as_expression(self)
                ->
                    <Self as
                    diesel::expression::AsExpression<diesel::sql_types::Nullable<crate::sql_types::Integer>>>::Expression {
                diesel::internal::derives::as_expression::Bound::new(self)
            }
        }
        impl<__DB>
            diesel::serialize::ToSql<diesel::sql_types::Nullable<crate::sql_types::Integer>,
            __DB> for AutoVacuumMode where __DB: diesel::backend::Backend,
            Self: diesel::serialize::ToSql<crate::sql_types::Integer, __DB> {
            fn to_sql<'__b>(&'__b self,
                out: &mut diesel::serialize::Output<'__b, '_, __DB>)
                -> diesel::serialize::Result {
                diesel::serialize::ToSql::<crate::sql_types::Integer,
                        __DB>::to_sql(self, out)
            }
        }
        impl diesel::expression::AsExpression<crate::sql_types::Integer> for
            AutoVacuumMode {
            type Expression =
                diesel::internal::derives::as_expression::Bound<crate::sql_types::Integer,
                Self>;
            fn as_expression(self)
                ->
                    <Self as
                    diesel::expression::AsExpression<crate::sql_types::Integer>>::Expression {
                diesel::internal::derives::as_expression::Bound::new(self)
            }
        }
        impl diesel::expression::AsExpression<diesel::sql_types::Nullable<crate::sql_types::Integer>>
            for AutoVacuumMode {
            type Expression =
                diesel::internal::derives::as_expression::Bound<diesel::sql_types::Nullable<crate::sql_types::Integer>,
                Self>;
            fn as_expression(self)
                ->
                    <Self as
                    diesel::expression::AsExpression<diesel::sql_types::Nullable<crate::sql_types::Integer>>>::Expression {
                diesel::internal::derives::as_expression::Bound::new(self)
            }
        }
        impl<__DB, __ST> diesel::deserialize::Queryable<__ST, __DB> for
            AutoVacuumMode where __DB: diesel::backend::Backend,
            __ST: diesel::sql_types::SingleValue,
            Self: diesel::deserialize::FromSql<__ST, __DB> {
            type Row = Self;
            fn build(row: Self) -> diesel::deserialize::Result<Self> {
                diesel::deserialize::Result::Ok(row)
            }
        }
    };crate::types::Enum)]
21#[diesel(sql_type = crate::sql_types::Integer)]
22#[non_exhaustive]
23#[repr(i32)]
24pub enum AutoVacuumMode {
25    /// Freed pages stay on the freelist and the file never shrinks (default).
26    None = 0,
27    /// Freed pages are reclaimed and the file truncated at every commit.
28    Full = 1,
29    /// Freelist bookkeeping is kept, pages are reclaimed only when
30    /// `incremental_vacuum` runs.
31    Incremental = 2,
32}
33
34/// The mode of a [`wal_checkpoint`](SqliteConnection::wal_checkpoint) run,
35/// matching the modes of
36/// [`sqlite3_wal_checkpoint_v2`](https://www.sqlite.org/c3ref/wal_checkpoint_v2.html).
37#[derive(#[automatically_derived]
impl ::core::fmt::Debug for WalCheckpointMode {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_c_like_enum_write_str(f,
            "Passive Full Restart Truncate Noop",
            match self {
                WalCheckpointMode::Passive => 0usize,
                WalCheckpointMode::Full => 8usize,
                WalCheckpointMode::Restart => 13usize,
                WalCheckpointMode::Truncate => 21usize,
                WalCheckpointMode::Noop => 30usize,
            })
    }
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for WalCheckpointMode { }
#[automatically_derived]
impl ::core::clone::Clone for WalCheckpointMode {
    #[inline]
    fn clone(&self) -> Self { *self }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for WalCheckpointMode { }Copy, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for WalCheckpointMode { }
#[automatically_derived]
impl ::core::cmp::PartialEq for WalCheckpointMode {
    #[inline]
    fn eq(&self, other: &Self) -> bool {
        ::core::intrinsics::discriminant_value(self) ==
            ::core::intrinsics::discriminant_value(other)
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for WalCheckpointMode { }Eq)]
38#[non_exhaustive]
39pub enum WalCheckpointMode {
40    /// Checkpoint what is possible without waiting on readers or writers.
41    Passive,
42    /// Wait until there is no writer and every reader reads from the most
43    /// recent snapshot, then checkpoint every frame.
44    Full,
45    /// Like [`Full`](Self::Full), then wait until no reader uses the WAL, so
46    /// the next writer restarts the log.
47    Restart,
48    /// Like [`Restart`](Self::Restart), then truncate the WAL file to zero
49    /// bytes.
50    Truncate,
51    /// Report the WAL state without checkpointing anything.
52    ///
53    /// Requires SQLite 3.51.0 or later. Older versions do not know this
54    /// mode and silently run a [`Passive`](Self::Passive) checkpoint
55    /// instead.
56    Noop,
57}
58
59/// The result of a [`wal_checkpoint`](SqliteConnection::wal_checkpoint) run.
60#[derive(#[automatically_derived]
impl ::core::fmt::Debug for WalCheckpointOutcome {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field3_finish(f,
            "WalCheckpointOutcome", "busy", &self.busy, "log_frames",
            &self.log_frames, "checkpointed_frames",
            &&self.checkpointed_frames)
    }
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for WalCheckpointOutcome { }
#[automatically_derived]
impl ::core::clone::Clone for WalCheckpointOutcome {
    #[inline]
    fn clone(&self) -> Self {
        let _: ::core::clone::AssertParamIsClone<bool>;
        let _: ::core::clone::AssertParamIsClone<Option<i64>>;
        let _: ::core::clone::AssertParamIsClone<Option<i64>>;
        *self
    }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for WalCheckpointOutcome { }Copy, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for WalCheckpointOutcome { }
#[automatically_derived]
impl ::core::cmp::PartialEq for WalCheckpointOutcome {
    #[inline]
    fn eq(&self, other: &Self) -> bool {
        self.busy == other.busy && self.log_frames == other.log_frames &&
            self.checkpointed_frames == other.checkpointed_frames
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for WalCheckpointOutcome {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<bool>;
        let _: ::core::cmp::AssertParamIsEq<Option<i64>>;
        let _: ::core::cmp::AssertParamIsEq<Option<i64>>;
    }
}Eq)]
61#[non_exhaustive]
62pub struct WalCheckpointOutcome {
63    /// Whether a busy reader or writer stopped the checkpoint early. Only
64    /// the blocking modes set it: [`Passive`](WalCheckpointMode::Passive)
65    /// reports `false` even when it left frames behind.
66    pub busy: bool,
67    /// Frames in the WAL after the checkpoint, `None` when the database is
68    /// not in WAL mode.
69    pub log_frames: Option<i64>,
70    /// Frames of the WAL moved into the database file, `None` when the
71    /// database is not in WAL mode. Counted within the current log, so a
72    /// [`Truncate`](WalCheckpointMode::Truncate) run reports `Some(0)`
73    /// because the log was emptied, not because nothing was moved.
74    pub checkpointed_frames: Option<i64>,
75}
76
77impl SqliteConnection {
78    /// Read the [`auto_vacuum`](AutoVacuumMode) mode of a database.
79    ///
80    /// `schema` selects an attached database by name, `None` reads `main`.
81    ///
82    /// ```rust
83    /// # include!("../../doctest_setup.rs");
84    /// # fn main() {
85    /// #     run_test().unwrap();
86    /// # }
87    /// # fn run_test() -> QueryResult<()> {
88    /// use diesel::sqlite::AutoVacuumMode;
89    /// let conn = &mut SqliteConnection::establish(":memory:").unwrap();
90    /// assert_eq!(conn.auto_vacuum(None)?, AutoVacuumMode::None);
91    /// #     Ok(())
92    /// # }
93    /// ```
94    pub fn auto_vacuum(&mut self, schema: Option<&str>) -> QueryResult<AutoVacuumMode> {
95        use crate::query_dsl::RunQueryDsl;
96        let query: Pragma<'_, crate::sql_types::Integer> = Pragma::new("auto_vacuum", schema);
97        query.get_result(self)
98    }
99
100    /// Set the [`auto_vacuum`](AutoVacuumMode) mode of a database.
101    ///
102    /// `schema` selects an attached database by name, `None` targets `main`.
103    /// Changing from or to [`AutoVacuumMode::None`] only takes effect on a
104    /// database with no tables yet, or after a subsequent `VACUUM`.
105    ///
106    /// ```rust
107    /// # include!("../../doctest_setup.rs");
108    /// # fn main() {
109    /// #     run_test().unwrap();
110    /// # }
111    /// # fn run_test() -> QueryResult<()> {
112    /// use diesel::sqlite::AutoVacuumMode;
113    /// let conn = &mut SqliteConnection::establish(":memory:").unwrap();
114    /// conn.set_auto_vacuum(None, AutoVacuumMode::Incremental)?;
115    /// assert_eq!(conn.auto_vacuum(None)?, AutoVacuumMode::Incremental);
116    /// #     Ok(())
117    /// # }
118    /// ```
119    pub fn set_auto_vacuum(
120        &mut self,
121        schema: Option<&str>,
122        mode: AutoVacuumMode,
123    ) -> QueryResult<()> {
124        use crate::query_dsl::RunQueryDsl;
125        // #[repr(i32)] guarantees the discriminant fits exactly in i32.
126        SetPragmaInt {
127            schema,
128            name: "auto_vacuum",
129            value: mode as i32,
130        }
131        .execute(self)
132        .map(|_| ())
133    }
134
135    /// Total number of pages in a database, via `PRAGMA page_count`.
136    ///
137    /// `schema` selects an attached database by name, `None` reads `main`.
138    /// Multiply by the page size for the size the database accounts for.
139    ///
140    /// ```rust
141    /// # include!("../../doctest_setup.rs");
142    /// # fn main() {
143    /// #     run_test().unwrap();
144    /// # }
145    /// # fn run_test() -> QueryResult<()> {
146    /// use diesel::connection::SimpleConnection;
147    /// let conn = &mut SqliteConnection::establish(":memory:").unwrap();
148    /// // An empty database occupies no pages until something is written.
149    /// assert_eq!(conn.page_count(None)?, 0);
150    /// conn.batch_execute("CREATE TABLE items (id INTEGER PRIMARY KEY)")?;
151    /// assert!(conn.page_count(None)? > 0);
152    /// #     Ok(())
153    /// # }
154    /// ```
155    pub fn page_count(&mut self, schema: Option<&str>) -> QueryResult<i64> {
156        self.read_pragma_count("page_count", schema)
157    }
158
159    /// Unused pages on a database's freelist, via `PRAGMA freelist_count`.
160    ///
161    /// `schema` selects an attached database by name, `None` reads `main`. A
162    /// growing freelist is reclaimable space, freed by `VACUUM`.
163    ///
164    /// ```rust
165    /// # include!("../../doctest_setup.rs");
166    /// # fn main() {
167    /// #     run_test().unwrap();
168    /// # }
169    /// # fn run_test() -> QueryResult<()> {
170    /// let conn = &mut SqliteConnection::establish(":memory:").unwrap();
171    /// assert_eq!(conn.freelist_count(None)?, 0);
172    /// #     Ok(())
173    /// # }
174    /// ```
175    pub fn freelist_count(&mut self, schema: Option<&str>) -> QueryResult<i64> {
176        self.read_pragma_count("freelist_count", schema)
177    }
178
179    fn read_pragma_count(
180        &mut self,
181        pragma: &'static str,
182        schema: Option<&str>,
183    ) -> QueryResult<i64> {
184        use crate::query_dsl::RunQueryDsl;
185
186        let query: Pragma<'_, crate::sql_types::BigInt> = Pragma::new(pragma, schema);
187        query.get_result(self)
188    }
189
190    /// Shrink a database by releasing freelist pages, without the full rewrite
191    /// [`VACUUM`](https://www.sqlite.org/lang_vacuum.html) performs.
192    ///
193    /// `schema` selects an attached database by name, `None` targets `main`. `pages`
194    /// bounds how many pages are reclaimed. As SQLite specifies, `None` or a value
195    /// below one clears the whole freelist, as does a bound larger than it. Only
196    /// databases in [`AutoVacuumMode::Incremental`] have anything to reclaim, on any
197    /// other mode this succeeds and does nothing.
198    ///
199    /// ```rust
200    /// # include!("../../doctest_setup.rs");
201    /// # fn main() {
202    /// #     run_test().unwrap();
203    /// # }
204    /// # fn run_test() -> QueryResult<()> {
205    /// use diesel::sqlite::AutoVacuumMode;
206    /// let conn = &mut SqliteConnection::establish(":memory:").unwrap();
207    /// conn.set_auto_vacuum(None, AutoVacuumMode::Incremental)?;
208    /// // Reclaim at most 8 pages, then whatever is left.
209    /// conn.incremental_vacuum(None, Some(8))?;
210    /// conn.incremental_vacuum(None, None)?;
211    /// #     Ok(())
212    /// # }
213    /// ```
214    pub fn incremental_vacuum(
215        &mut self,
216        schema: Option<&str>,
217        pages: Option<u32>,
218    ) -> QueryResult<()> {
219        use crate::connection::SimpleConnection;
220        use crate::query_builder::QueryBuilder;
221        use crate::sqlite::SqliteQueryBuilder;
222
223        // SQLite frees one page per step of this statement, so it only empties the
224        // freelist when run to completion. `batch_execute` uses `sqlite3_exec`, which
225        // does that. A prepared statement would not: `StatementUse::run` steps once,
226        // which frees a single page and silently leaves the rest.
227        let mut query = SqliteQueryBuilder::new();
228        query.push_sql("PRAGMA ");
229        query.push_identifier(schema.unwrap_or("main"))?;
230        query.push_sql(".incremental_vacuum");
231        if let Some(pages) = pages {
232            query.push_sql("(");
233            query.push_sql(&pages.to_string());
234            query.push_sql(")");
235        }
236        self.batch_execute(&query.finish())
237    }
238
239    /// Rebuild a database, repacking it into the smallest space it can occupy.
240    ///
241    /// `schema` selects an attached database by name, `None` targets `main`.
242    ///
243    /// This cannot run inside a transaction, needs free space of up to twice the size
244    /// of the database while it runs, and renumbers the implicit `rowid` of any table
245    /// declared without an `INTEGER PRIMARY KEY`.
246    ///
247    /// Naming a schema requires SQLite 3.24.0 or later, otherwise returns an error.
248    ///
249    /// ```rust
250    /// # include!("../../doctest_setup.rs");
251    /// # fn main() {
252    /// #     run_test().unwrap();
253    /// # }
254    /// # fn run_test() -> QueryResult<()> {
255    /// let conn = &mut SqliteConnection::establish(":memory:").unwrap();
256    /// conn.vacuum(None)?;
257    /// #     Ok(())
258    /// # }
259    /// ```
260    pub fn vacuum(&mut self, schema: Option<&str>) -> QueryResult<()> {
261        use crate::query_dsl::RunQueryDsl;
262
263        Vacuum { schema, into: None }.execute(self).map(|_| ())
264    }
265
266    /// Write a vacuumed copy of a database to `path`, leaving the original untouched.
267    ///
268    /// This is SQLite's online backup: the copy is consistent, defragmented, and taken
269    /// without blocking readers. `schema` selects an attached database by name, `None`
270    /// copies `main`. The path is a bind parameter, so it needs no quoting.
271    ///
272    /// `path` may name a file that does not exist or one that is empty, but writing
273    /// over an existing database fails rather than replacing it.
274    ///
275    /// Requires SQLite 3.27.0 or later, otherwise returns an error.
276    ///
277    /// ```rust
278    /// # include!("../../doctest_setup.rs");
279    /// # // This test creates a temporary directory, which is not available
280    /// # // when miri runs with isolation enabled.
281    /// # #[cfg(miri)]
282    /// # fn main() {}
283    /// # #[cfg(not(miri))]
284    /// # fn main() {
285    /// #     run_test().unwrap();
286    /// # }
287    /// # fn run_test() -> QueryResult<()> {
288    /// # let dir = tempfile::tempdir().unwrap();
289    /// # let backup = dir.path().join("backup.db");
290    /// let conn = &mut SqliteConnection::establish(":memory:").unwrap();
291    /// conn.vacuum_into(None, backup.to_str().unwrap())?;
292    /// # assert!(backup.exists());
293    /// #     Ok(())
294    /// # }
295    /// ```
296    pub fn vacuum_into(&mut self, schema: Option<&str>, path: &str) -> QueryResult<()> {
297        use crate::query_dsl::RunQueryDsl;
298
299        Vacuum {
300            schema,
301            into: Some(path),
302        }
303        .execute(self)
304        .map(|_| ())
305    }
306
307    /// Checkpoint the [write-ahead log](https://www.sqlite.org/wal.html),
308    /// moving committed frames from the WAL file into the database file.
309    ///
310    /// `schema` selects one attached database by name. Unlike the other
311    /// maintenance helpers, `None` does not mean `main`: SQLite defines the
312    /// unqualified pragma to checkpoint every attached database. With
313    /// `None` and several attached databases the C API leaves the frame
314    /// counts undefined.
315    ///
316    /// A checkpoint stopped early by a reader or writer on another
317    /// connection is not an error: it sets
318    /// [`busy`](WalCheckpointOutcome::busy). On a database that is not in
319    /// WAL mode the call succeeds with both frame counts `None`, so it is
320    /// safe to issue unconditionally. Inside a transaction on its own
321    /// connection it fails with `SQLITE_LOCKED`.
322    ///
323    /// The mode argument requires SQLite 3.7.6 or later,
324    /// [`Truncate`](WalCheckpointMode::Truncate) requires 3.8.8 or later,
325    /// and [`Noop`](WalCheckpointMode::Noop) requires 3.51.0 or later.
326    /// Older versions do not report an error and treat an unrecognized
327    /// mode as [`Passive`](WalCheckpointMode::Passive).
328    ///
329    /// ```rust
330    /// # include!("../../doctest_setup.rs");
331    /// #
332    /// # // This test creates a temporary directory, which is not available
333    /// # // when miri runs with isolation enabled.
334    /// # #[cfg(miri)]
335    /// # fn main() {}
336    /// # #[cfg(not(miri))]
337    /// # fn main() {
338    /// #     run_test().unwrap();
339    /// # }
340    /// #
341    /// # fn run_test() -> QueryResult<()> {
342    /// use diesel::connection::SimpleConnection;
343    /// use diesel::sqlite::WalCheckpointMode;
344    /// # let dir = tempfile::tempdir().unwrap();
345    /// # let path = dir.path().join("app.db");
346    /// let conn = &mut SqliteConnection::establish(path.to_str().unwrap()).unwrap();
347    /// conn.batch_execute("PRAGMA journal_mode = WAL")?;
348    /// conn.batch_execute("CREATE TABLE logs (line TEXT NOT NULL)")?;
349    ///
350    /// let outcome = conn.wal_checkpoint(None, WalCheckpointMode::Truncate)?;
351    /// assert!(!outcome.busy);
352    /// // The whole WAL was moved into the database file and the log truncated.
353    /// assert_eq!(outcome.log_frames, Some(0));
354    /// assert_eq!(outcome.checkpointed_frames, Some(0));
355    /// #     Ok(())
356    /// # }
357    /// ```
358    pub fn wal_checkpoint(
359        &mut self,
360        schema: Option<&str>,
361        mode: WalCheckpointMode,
362    ) -> QueryResult<WalCheckpointOutcome> {
363        use crate::query_dsl::RunQueryDsl;
364
365        let (busy, log_frames, checkpointed_frames) =
366            WalCheckpoint { schema, mode }.get_result::<(i32, i64, i64)>(self)?;
367        Ok(WalCheckpointOutcome {
368            busy: busy != 0,
369            // On a database not in WAL mode both counts come back as -1.
370            log_frames: (log_frames >= 0).then_some(log_frames),
371            checkpointed_frames: (checkpointed_frames >= 0).then_some(checkpointed_frames),
372        })
373    }
374}
375
376// A `PRAGMA` accepts no bind parameters, neither for the schema it targets nor for the
377// value it assigns, so the schema is rendered as a quoted identifier by the query
378// builder. `name` is always a constant chosen here, never caller data.
379struct Pragma<'a, ST> {
380    schema: Option<&'a str>,
381    name: &'static str,
382    sql_type: PhantomData<ST>,
383}
384
385impl<'a, ST> Pragma<'a, ST> {
386    fn new(name: &'static str, schema: Option<&'a str>) -> Self {
387        Pragma {
388            schema,
389            name,
390            sql_type: PhantomData,
391        }
392    }
393}
394
395impl<ST> QueryFragment<Sqlite> for Pragma<'_, ST> {
396    fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Sqlite>) -> QueryResult<()> {
397        out.push_sql("PRAGMA ");
398        out.push_identifier(self.schema.unwrap_or("main"))?;
399        out.push_sql(".");
400        out.push_sql(self.name);
401        Ok(())
402    }
403}
404
405// The schema name is runtime data, so the rendered SQL is not determined by the type.
406impl<ST> QueryId for Pragma<'_, ST> {
407    type QueryId = ();
408
409    const HAS_STATIC_QUERY_ID: bool = false;
410}
411
412impl<ST> Query for Pragma<'_, ST> {
413    type SqlType = ST;
414}
415
416impl<ST> RunQueryDslSupport for Pragma<'_, ST> {}
417
418// `PRAGMA name = value` takes no bind parameter for the value either, so the integer is
419// rendered as a literal.
420struct SetPragmaInt<'a> {
421    schema: Option<&'a str>,
422    name: &'static str,
423    value: i32,
424}
425
426impl QueryFragment<Sqlite> for SetPragmaInt<'_> {
427    fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Sqlite>) -> QueryResult<()> {
428        out.push_sql("PRAGMA ");
429        out.push_identifier(self.schema.unwrap_or("main"))?;
430        out.push_sql(".");
431        out.push_sql(self.name);
432        out.push_sql(" = ");
433        out.push_sql(&self.value.to_string());
434        Ok(())
435    }
436}
437
438impl QueryId for SetPragmaInt<'_> {
439    type QueryId = ();
440
441    const HAS_STATIC_QUERY_ID: bool = false;
442}
443
444impl RunQueryDslSupport for SetPragmaInt<'_> {}
445
446// `VACUUM` names its schema as an identifier, so that operand is quoted by the query
447// builder, while the `INTO` destination is an expression and binds normally.
448struct Vacuum<'a> {
449    schema: Option<&'a str>,
450    into: Option<&'a str>,
451}
452
453impl QueryFragment<Sqlite> for Vacuum<'_> {
454    fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Sqlite>) -> QueryResult<()> {
455        out.push_sql("VACUUM ");
456        out.push_identifier(self.schema.unwrap_or("main"))?;
457        if let Some(into) = self.into {
458            out.push_sql(" INTO ");
459            out.push_bind_param::<crate::sql_types::Text, _>(into)?;
460        }
461        Ok(())
462    }
463}
464
465// The schema name is runtime data, so the rendered SQL is not determined by the type.
466impl QueryId for Vacuum<'_> {
467    type QueryId = ();
468
469    const HAS_STATIC_QUERY_ID: bool = false;
470}
471
472impl RunQueryDslSupport for Vacuum<'_> {}
473
474// Like `Pragma`, no operand can be a bind parameter. Unlike `Pragma`, a
475// `None` schema stays unqualified on purpose: the unqualified pragma
476// checkpoints every attached database, while a qualified one targets a
477// single schema. The whole checkpoint runs on the first step of the
478// statement and yields exactly one row, so a prepared statement works here
479// (unlike `incremental_vacuum`).
480struct WalCheckpoint<'a> {
481    schema: Option<&'a str>,
482    mode: WalCheckpointMode,
483}
484
485impl QueryFragment<Sqlite> for WalCheckpoint<'_> {
486    fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Sqlite>) -> QueryResult<()> {
487        out.push_sql("PRAGMA ");
488        if let Some(schema) = self.schema {
489            out.push_identifier(schema)?;
490            out.push_sql(".");
491        }
492        out.push_sql(match self.mode {
493            WalCheckpointMode::Passive => "wal_checkpoint(PASSIVE)",
494            WalCheckpointMode::Full => "wal_checkpoint(FULL)",
495            WalCheckpointMode::Restart => "wal_checkpoint(RESTART)",
496            WalCheckpointMode::Truncate => "wal_checkpoint(TRUNCATE)",
497            WalCheckpointMode::Noop => "wal_checkpoint(NOOP)",
498        });
499        Ok(())
500    }
501}
502
503// The schema name and mode are runtime data, so the rendered SQL is not determined by the type.
504impl QueryId for WalCheckpoint<'_> {
505    type QueryId = ();
506
507    const HAS_STATIC_QUERY_ID: bool = false;
508}
509
510impl Query for WalCheckpoint<'_> {
511    type SqlType = (
512        crate::sql_types::Integer,
513        crate::sql_types::BigInt,
514        crate::sql_types::BigInt,
515    );
516}
517
518impl RunQueryDslSupport for WalCheckpoint<'_> {}
519
520#[cfg(test)]
521mod tests {
522    use super::*;
523    use crate::connection::SimpleConnection;
524    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
525    use crate::dsl::sql;
526    use crate::prelude::*;
527    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
528    use crate::sql_types::Integer;
529
530    fn connection() -> SqliteConnection {
531        SqliteConnection::establish(":memory:").unwrap()
532    }
533
534    #[diesel_test_helper::test]
535    fn auto_vacuum_all_modes_roundtrip_on_fresh_database() {
536        for mode in [
537            AutoVacuumMode::None,
538            AutoVacuumMode::Full,
539            AutoVacuumMode::Incremental,
540        ] {
541            let conn = &mut connection();
542            conn.set_auto_vacuum(None, mode).unwrap();
543            assert_eq!(mode, conn.auto_vacuum(None).unwrap());
544        }
545    }
546
547    #[diesel_test_helper::test]
548    fn auto_vacuum_incremental_sticks_across_schema_creation() {
549        let conn = &mut connection();
550        conn.set_auto_vacuum(None, AutoVacuumMode::Incremental)
551            .unwrap();
552        assert_eq!(AutoVacuumMode::Incremental, conn.auto_vacuum(None).unwrap());
553
554        crate::sql_query("CREATE TABLE t (id INTEGER PRIMARY KEY)")
555            .execute(conn)
556            .unwrap();
557        assert_eq!(
558            AutoVacuumMode::Incremental,
559            conn.auto_vacuum(None).unwrap(),
560            "the mode survives once the schema exists"
561        );
562    }
563
564    #[diesel_test_helper::test]
565    fn auto_vacuum_change_from_none_requires_vacuum_on_populated_database() {
566        let conn = &mut connection();
567        crate::sql_query("CREATE TABLE t (id INTEGER PRIMARY KEY)")
568            .execute(conn)
569            .unwrap();
570        crate::sql_query("INSERT INTO t (id) VALUES (1)")
571            .execute(conn)
572            .unwrap();
573        assert_eq!(AutoVacuumMode::None, conn.auto_vacuum(None).unwrap());
574
575        // On a populated database the switch away from `None` is silently
576        // deferred until a full rewrite.
577        conn.set_auto_vacuum(None, AutoVacuumMode::Full).unwrap();
578        assert_eq!(
579            AutoVacuumMode::None,
580            conn.auto_vacuum(None).unwrap(),
581            "the change does not take effect without a VACUUM"
582        );
583
584        crate::sql_query("VACUUM").execute(conn).unwrap();
585        assert_eq!(
586            AutoVacuumMode::Full,
587            conn.auto_vacuum(None).unwrap(),
588            "VACUUM rewrites the file and applies the mode"
589        );
590    }
591
592    #[diesel_test_helper::test]
593    fn auto_vacuum_targets_the_named_attached_database() {
594        let conn = &mut connection();
595        crate::sql_query("ATTACH DATABASE ':memory:' AS aux")
596            .execute(conn)
597            .unwrap();
598
599        conn.set_auto_vacuum(Some("aux"), AutoVacuumMode::Full)
600            .unwrap();
601        assert_eq!(AutoVacuumMode::Full, conn.auto_vacuum(Some("aux")).unwrap());
602        assert_eq!(
603            AutoVacuumMode::None,
604            conn.auto_vacuum(None).unwrap(),
605            "main keeps its own default"
606        );
607    }
608
609    #[diesel_test_helper::test]
610    fn auto_vacuum_schema_name_with_double_quote_is_handled() {
611        let conn = &mut connection();
612        let schema = r#"we"ird"#;
613        crate::sql_query(alloc::format!(
614            r#"ATTACH DATABASE ':memory:' AS "{}""#,
615            schema.replace('"', "\"\"")
616        ))
617        .execute(conn)
618        .unwrap();
619
620        conn.set_auto_vacuum(Some(schema), AutoVacuumMode::Incremental)
621            .unwrap();
622        assert_eq!(
623            AutoVacuumMode::Incremental,
624            conn.auto_vacuum(Some(schema)).unwrap()
625        );
626    }
627
628    table! {
629        pragma_probe (id) {
630            id -> Integer,
631            payload -> Text,
632        }
633    }
634
635    table! {
636        aux.aux_pragma_probe (id) {
637            id -> Integer,
638            payload -> Text,
639        }
640    }
641
642    const PROBE_TABLE: &str =
643        "CREATE TABLE pragma_probe (id INTEGER PRIMARY KEY, payload TEXT NOT NULL)";
644
645    const AUX_PROBE_TABLE: &str =
646        "CREATE TABLE aux.aux_pragma_probe (id INTEGER PRIMARY KEY, payload TEXT NOT NULL)";
647
648    // Large enough to spill onto overflow pages, so the database outgrows a single page
649    // and leaves reclaimable pages behind once the row is deleted.
650    fn overflowing_payload() -> String {
651        "x".repeat(64 * 1024)
652    }
653
654    fn insert_overflowing_row(conn: &mut SqliteConnection) {
655        crate::insert_into(pragma_probe::table)
656            .values((
657                pragma_probe::id.eq(1),
658                pragma_probe::payload.eq(overflowing_payload()),
659            ))
660            .execute(conn)
661            .unwrap();
662    }
663
664    #[diesel_test_helper::test]
665    fn page_count_is_positive_and_grows() {
666        let conn = &mut connection();
667        conn.batch_execute(PROBE_TABLE).unwrap();
668        let initial = conn.page_count(None).unwrap();
669        assert!(initial > 0, "an initialized database has at least one page");
670
671        insert_overflowing_row(conn);
672
673        assert!(
674            conn.page_count(None).unwrap() > initial,
675            "a row spanning overflow pages grows the page count"
676        );
677    }
678
679    #[diesel_test_helper::test]
680    fn freelist_count_tracks_reclaimable_space() {
681        let conn = &mut connection();
682        assert_eq!(
683            0,
684            conn.freelist_count(None).unwrap(),
685            "a fresh database has an empty freelist"
686        );
687
688        conn.batch_execute(PROBE_TABLE).unwrap();
689        insert_overflowing_row(conn);
690
691        crate::delete(pragma_probe::table).execute(conn).unwrap();
692        assert!(
693            conn.freelist_count(None).unwrap() > 0,
694            "deleting the row leaves reclaimable pages on the freelist"
695        );
696
697        // `VACUUM` has no query DSL equivalent.
698        crate::sql_query("VACUUM").execute(conn).unwrap();
699        assert_eq!(
700            0,
701            conn.freelist_count(None).unwrap(),
702            "VACUUM reclaims the freelist"
703        );
704    }
705
706    #[diesel_test_helper::test]
707    fn schema_targets_the_named_attached_database() {
708        let conn = &mut connection();
709        conn.batch_execute(PROBE_TABLE).unwrap();
710        conn.attach_database(":memory:", "aux").unwrap();
711        conn.batch_execute(AUX_PROBE_TABLE).unwrap();
712        crate::insert_into(aux_pragma_probe::table)
713            .values((
714                aux_pragma_probe::id.eq(1),
715                aux_pragma_probe::payload.eq(overflowing_payload()),
716            ))
717            .execute(conn)
718            .unwrap();
719
720        let main_pages = conn.page_count(None).unwrap();
721        let aux_pages = conn.page_count(Some("aux")).unwrap();
722        assert!(
723            aux_pages > main_pages,
724            "the attached database holds the data, main stays small"
725        );
726        assert_eq!(
727            main_pages,
728            conn.page_count(Some("main")).unwrap(),
729            "an explicit main matches the default"
730        );
731    }
732
733    #[diesel_test_helper::test]
734    fn schema_name_with_backtick_is_escaped() {
735        // The query builder quotes SQLite identifiers with backticks, so a backtick is
736        // the character that has to be doubled.
737        let conn = &mut connection();
738        let schema = "back`tick";
739        conn.attach_database(":memory:", schema).unwrap();
740        conn.batch_execute("CREATE TABLE `back``tick`.probe (id INTEGER PRIMARY KEY)")
741            .unwrap();
742
743        assert!(conn.page_count(Some(schema)).unwrap() > 0);
744        assert_eq!(0, conn.freelist_count(Some(schema)).unwrap());
745    }
746
747    #[cfg(not(miri))] // ffi string access
748    #[diesel_test_helper::test]
749    fn unknown_schema_is_reported_as_an_error() {
750        let conn = &mut connection();
751
752        assert!(conn.page_count(Some("nope")).is_err());
753        assert!(conn.freelist_count(Some("nope")).is_err());
754    }
755
756    // Leaves many pages on the freelist, so `incremental_vacuum` has something to
757    // reclaim and a bound smaller than the freelist is meaningful.
758    fn grow_then_empty_freelist(conn: &mut SqliteConnection) {
759        conn.batch_execute(PROBE_TABLE).unwrap();
760        let rows = (1..=200)
761            .map(|id| {
762                (
763                    pragma_probe::id.eq(id),
764                    pragma_probe::payload.eq("x".repeat(4000)),
765                )
766            })
767            .collect::<Vec<_>>();
768        crate::insert_into(pragma_probe::table)
769            .values(rows)
770            .execute(conn)
771            .unwrap();
772        crate::delete(pragma_probe::table).execute(conn).unwrap();
773    }
774
775    // The same, in an attached schema.
776    fn grow_then_empty_aux_freelist(conn: &mut SqliteConnection) {
777        conn.batch_execute(AUX_PROBE_TABLE).unwrap();
778        let rows = (1..=200)
779            .map(|id| {
780                (
781                    aux_pragma_probe::id.eq(id),
782                    aux_pragma_probe::payload.eq("x".repeat(4000)),
783                )
784            })
785            .collect::<Vec<_>>();
786        crate::insert_into(aux_pragma_probe::table)
787            .values(rows)
788            .execute(conn)
789            .unwrap();
790        crate::delete(aux_pragma_probe::table)
791            .execute(conn)
792            .unwrap();
793    }
794
795    #[diesel_test_helper::test]
796    fn incremental_vacuum_clears_the_whole_freelist() {
797        let conn = &mut connection();
798        conn.set_auto_vacuum(None, AutoVacuumMode::Incremental)
799            .unwrap();
800        grow_then_empty_freelist(conn);
801        assert!(
802            conn.freelist_count(None).unwrap() > 1,
803            "the deleted rows should leave many pages on the freelist"
804        );
805
806        conn.incremental_vacuum(None, None).unwrap();
807
808        // Stepping the pragma only once would free a single page and leave the rest, so
809        // this also pins that the statement is driven to completion.
810        assert_eq!(0, conn.freelist_count(None).unwrap());
811    }
812
813    #[diesel_test_helper::test]
814    fn incremental_vacuum_reclaims_at_most_the_requested_pages() {
815        let conn = &mut connection();
816        conn.set_auto_vacuum(None, AutoVacuumMode::Incremental)
817            .unwrap();
818        grow_then_empty_freelist(conn);
819        let before = conn.freelist_count(None).unwrap();
820        assert!(before > 10, "the bound has to be smaller than the freelist");
821
822        conn.incremental_vacuum(None, Some(10)).unwrap();
823
824        let after = conn.freelist_count(None).unwrap();
825        assert!(after >= before - 10, "at most ten pages may be reclaimed");
826        assert!(after < before, "some pages should have been reclaimed");
827    }
828
829    #[diesel_test_helper::test]
830    fn incremental_vacuum_is_a_no_op_outside_incremental_mode() {
831        let conn = &mut connection();
832        assert_eq!(AutoVacuumMode::None, conn.auto_vacuum(None).unwrap());
833        grow_then_empty_freelist(conn);
834        let before = conn.freelist_count(None).unwrap();
835        assert!(before > 0);
836
837        conn.incremental_vacuum(None, None).unwrap();
838
839        assert_eq!(
840            before,
841            conn.freelist_count(None).unwrap(),
842            "a database that is not in incremental mode keeps its freelist"
843        );
844    }
845
846    #[diesel_test_helper::test]
847    fn incremental_vacuum_targets_the_named_attached_database() {
848        let conn = &mut connection();
849        conn.attach_database(":memory:", "aux").unwrap();
850        conn.set_auto_vacuum(Some("aux"), AutoVacuumMode::Incremental)
851            .unwrap();
852
853        grow_then_empty_aux_freelist(conn);
854        assert!(conn.freelist_count(Some("aux")).unwrap() > 0);
855
856        conn.incremental_vacuum(Some("aux"), None).unwrap();
857
858        assert_eq!(0, conn.freelist_count(Some("aux")).unwrap());
859    }
860
861    #[diesel_test_helper::test]
862    fn incremental_vacuum_escapes_a_backtick_in_the_schema_name() {
863        // An unquoted identifier would be a syntax error, and the wrong quoting would
864        // address a different database.
865        let conn = &mut connection();
866        let schema = "back`tick";
867        conn.attach_database(":memory:", schema).unwrap();
868
869        conn.incremental_vacuum(Some(schema), None).unwrap();
870
871        assert_eq!(0, conn.freelist_count(Some(schema)).unwrap());
872    }
873
874    #[diesel_test_helper::test]
875    fn incremental_vacuum_of_zero_pages_clears_everything() {
876        // SQLite specifies that a bound below one clears the whole freelist.
877        let conn = &mut connection();
878        conn.set_auto_vacuum(None, AutoVacuumMode::Incremental)
879            .unwrap();
880        grow_then_empty_freelist(conn);
881        assert!(conn.freelist_count(None).unwrap() > 0);
882
883        conn.incremental_vacuum(None, Some(0)).unwrap();
884
885        assert_eq!(0, conn.freelist_count(None).unwrap());
886    }
887
888    #[cfg(not(miri))] // ffi string access
889    #[diesel_test_helper::test]
890    fn incremental_vacuum_of_an_unknown_schema_is_an_error() {
891        let conn = &mut connection();
892
893        assert!(conn.incremental_vacuum(Some("nope"), None).is_err());
894    }
895
896    // Leaves the database holding one small row but occupying many pages, so a rebuild
897    // has something to reclaim.
898    fn fill_then_delete(conn: &mut SqliteConnection) {
899        conn.batch_execute(PROBE_TABLE).unwrap();
900        crate::insert_into(pragma_probe::table)
901            .values((
902                pragma_probe::id.eq(1),
903                pragma_probe::payload.eq("x".repeat(256 * 1024)),
904            ))
905            .execute(conn)
906            .unwrap();
907        crate::delete(pragma_probe::table).execute(conn).unwrap();
908        crate::insert_into(pragma_probe::table)
909            .values((pragma_probe::id.eq(2), pragma_probe::payload.eq("kept")))
910            .execute(conn)
911            .unwrap();
912    }
913
914    // The same, in an attached schema.
915    fn fill_then_delete_aux(conn: &mut SqliteConnection) {
916        conn.batch_execute(AUX_PROBE_TABLE).unwrap();
917        crate::insert_into(aux_pragma_probe::table)
918            .values((
919                aux_pragma_probe::id.eq(1),
920                aux_pragma_probe::payload.eq("x".repeat(256 * 1024)),
921            ))
922            .execute(conn)
923            .unwrap();
924        crate::delete(aux_pragma_probe::table)
925            .execute(conn)
926            .unwrap();
927    }
928
929    #[diesel_test_helper::test]
930    fn vacuum_repacks_the_database() {
931        let conn = &mut connection();
932        fill_then_delete(conn);
933        let before = conn.page_count(None).unwrap();
934        assert!(before > 1);
935
936        conn.vacuum(None).unwrap();
937
938        assert!(
939            conn.page_count(None).unwrap() < before,
940            "rebuilding should release the pages the deleted row occupied"
941        );
942        assert_eq!(
943            1,
944            pragma_probe::table.count().get_result::<i64>(conn).unwrap(),
945            "the surviving row is still there"
946        );
947    }
948
949    #[diesel_test_helper::test]
950    fn vacuum_targets_the_named_attached_database() {
951        let conn = &mut connection();
952        conn.attach_database(":memory:", "aux").unwrap();
953        fill_then_delete_aux(conn);
954        let before = conn.page_count(Some("aux")).unwrap();
955        assert!(before > 1);
956
957        conn.vacuum(Some("aux")).unwrap();
958
959        assert!(conn.page_count(Some("aux")).unwrap() < before);
960    }
961
962    #[cfg(not(miri))]
963    #[diesel_test_helper::test]
964    fn vacuum_inside_a_transaction_is_an_error() {
965        use crate::connection::Connection;
966
967        let conn = &mut connection();
968        let result: QueryResult<()> = conn.transaction(|conn| conn.vacuum(None));
969
970        assert!(result.is_err());
971    }
972
973    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
974    #[diesel_test_helper::test]
975    fn vacuum_into_writes_a_readable_copy_through_a_quoted_path() {
976        let dir = tempfile::tempdir().unwrap();
977        // A quote in the path would break a hand-assembled statement. It is a bind
978        // parameter, so it is taken verbatim.
979        let destination = dir.path().join("o'brien backup.db");
980
981        let conn = &mut connection();
982        conn.batch_execute(PROBE_TABLE).unwrap();
983        crate::insert_into(pragma_probe::table)
984            .values((pragma_probe::id.eq(1), pragma_probe::payload.eq("copied")))
985            .execute(conn)
986            .unwrap();
987
988        conn.vacuum_into(None, destination.to_str().unwrap())
989            .unwrap();
990
991        let copy = &mut SqliteConnection::establish(destination.to_str().unwrap()).unwrap();
992        assert_eq!(
993            "copied",
994            pragma_probe::table
995                .select(pragma_probe::payload)
996                .get_result::<String>(copy)
997                .unwrap()
998        );
999    }
1000
1001    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
1002    #[diesel_test_helper::test]
1003    fn vacuum_into_refuses_to_overwrite_an_existing_database() {
1004        let dir = tempfile::tempdir().unwrap();
1005        let destination = dir.path().join("occupied.db");
1006        {
1007            let occupied = &mut SqliteConnection::establish(destination.to_str().unwrap()).unwrap();
1008            occupied.batch_execute(PROBE_TABLE).unwrap();
1009        }
1010
1011        let conn = &mut connection();
1012        conn.batch_execute(PROBE_TABLE).unwrap();
1013
1014        assert!(
1015            conn.vacuum_into(None, destination.to_str().unwrap())
1016                .is_err()
1017        );
1018    }
1019
1020    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
1021    #[diesel_test_helper::test]
1022    fn vacuum_into_copies_the_named_attached_database() {
1023        let dir = tempfile::tempdir().unwrap();
1024        let destination = dir.path().join("aux copy.db");
1025
1026        let conn = &mut connection();
1027        conn.attach_database(":memory:", "aux").unwrap();
1028        conn.batch_execute(AUX_PROBE_TABLE).unwrap();
1029        crate::insert_into(aux_pragma_probe::table)
1030            .values((
1031                aux_pragma_probe::id.eq(7),
1032                aux_pragma_probe::payload.eq("copied"),
1033            ))
1034            .execute(conn)
1035            .unwrap();
1036
1037        conn.vacuum_into(Some("aux"), destination.to_str().unwrap())
1038            .unwrap();
1039
1040        let copy = &mut SqliteConnection::establish(destination.to_str().unwrap()).unwrap();
1041        // In the copy the table sits in `main`, while `aux_pragma_probe` is declared
1042        // schema-qualified, so this one read cannot go through it.
1043        let id = sql::<Integer>("SELECT id FROM aux_pragma_probe")
1044            .get_result::<i32>(copy)
1045            .unwrap();
1046        assert_eq!(7, id);
1047    }
1048
1049    #[diesel_test_helper::test]
1050    fn vacuum_escapes_a_backtick_in_the_schema_name() {
1051        let conn = &mut connection();
1052        let schema = "back`tick";
1053        conn.attach_database(":memory:", schema).unwrap();
1054
1055        conn.vacuum(Some(schema)).unwrap();
1056    }
1057
1058    #[diesel_test_helper::test]
1059    fn vacuuming_two_schemas_rebuilds_each_of_them() {
1060        // The schema is part of the rendered SQL, so the two calls must not share a
1061        // prepared statement. If they did, the second would rebuild the first's
1062        // database again and leave this one untouched.
1063        let conn = &mut connection();
1064        fill_then_delete(conn);
1065        conn.attach_database(":memory:", "aux").unwrap();
1066        fill_then_delete_aux(conn);
1067
1068        let main_before = conn.page_count(None).unwrap();
1069        let aux_before = conn.page_count(Some("aux")).unwrap();
1070
1071        conn.vacuum(None).unwrap();
1072        conn.vacuum(Some("aux")).unwrap();
1073
1074        assert!(
1075            conn.page_count(None).unwrap() < main_before,
1076            "main was rebuilt"
1077        );
1078        assert!(
1079            conn.page_count(Some("aux")).unwrap() < aux_before,
1080            "aux was rebuilt too, not main a second time"
1081        );
1082    }
1083
1084    // WAL requires a real file.
1085    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
1086    fn wal_connection(path: &std::path::Path) -> SqliteConnection {
1087        let mut conn = SqliteConnection::establish(path.to_str().unwrap()).unwrap();
1088        conn.batch_execute("PRAGMA journal_mode = WAL").unwrap();
1089        conn
1090    }
1091
1092    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
1093    #[diesel_test_helper::test]
1094    fn wal_checkpoint_truncate_reports_an_emptied_wal() {
1095        let dir = tempfile::tempdir().unwrap();
1096        let conn = &mut wal_connection(&dir.path().join("wal.db"));
1097        conn.batch_execute(PROBE_TABLE).unwrap();
1098        insert_overflowing_row(conn);
1099
1100        let outcome = conn
1101            .wal_checkpoint(None, WalCheckpointMode::Truncate)
1102            .unwrap();
1103
1104        assert!(!outcome.busy);
1105        assert_eq!(Some(0), outcome.log_frames, "the WAL file was truncated");
1106        assert_eq!(Some(0), outcome.checkpointed_frames);
1107    }
1108
1109    #[diesel_test_helper::test]
1110    fn wal_checkpoint_outside_wal_mode_reports_no_frames() {
1111        let conn = &mut connection();
1112
1113        let outcome = conn
1114            .wal_checkpoint(None, WalCheckpointMode::Truncate)
1115            .unwrap();
1116
1117        assert!(!outcome.busy);
1118        assert_eq!(None, outcome.log_frames);
1119        assert_eq!(None, outcome.checkpointed_frames);
1120    }
1121
1122    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
1123    #[diesel_test_helper::test]
1124    fn wal_checkpoint_accepts_every_mode() {
1125        let dir = tempfile::tempdir().unwrap();
1126        let conn = &mut wal_connection(&dir.path().join("modes.db"));
1127        conn.batch_execute(PROBE_TABLE).unwrap();
1128
1129        for (row, mode) in [
1130            WalCheckpointMode::Passive,
1131            WalCheckpointMode::Full,
1132            WalCheckpointMode::Restart,
1133            WalCheckpointMode::Truncate,
1134            WalCheckpointMode::Noop,
1135        ]
1136        .into_iter()
1137        .enumerate()
1138        {
1139            // A fresh row per round gives every mode frames to move.
1140            crate::insert_into(pragma_probe::table)
1141                .values((
1142                    pragma_probe::id.eq(i32::try_from(row).unwrap() + 1),
1143                    pragma_probe::payload.eq("row"),
1144                ))
1145                .execute(conn)
1146                .unwrap();
1147
1148            let outcome = conn.wal_checkpoint(None, mode).unwrap();
1149            assert!(!outcome.busy, "{mode:?} had no competing readers");
1150            assert!(
1151                outcome.log_frames.is_some(),
1152                "{mode:?} ran on a WAL database"
1153            );
1154            assert!(outcome.checkpointed_frames.is_some());
1155            assert!(
1156                outcome.checkpointed_frames <= outcome.log_frames,
1157                "{mode:?}: checkpointed frames cannot exceed the log size"
1158            );
1159        }
1160    }
1161
1162    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
1163    #[diesel_test_helper::test]
1164    fn wal_checkpoint_noop_reports_state_without_moving_frames() {
1165        let dir = tempfile::tempdir().unwrap();
1166        let conn = &mut wal_connection(&dir.path().join("noop.db"));
1167
1168        // NOOP exists since SQLite 3.51.0, older versions run PASSIVE instead.
1169        if conn.version() < crate::connection::DatabaseVersion::new(3, 51, 0) {
1170            return;
1171        }
1172
1173        conn.batch_execute(PROBE_TABLE).unwrap();
1174        conn.wal_checkpoint(None, WalCheckpointMode::Truncate)
1175            .unwrap();
1176        crate::insert_into(pragma_probe::table)
1177            .values((pragma_probe::id.eq(1), pragma_probe::payload.eq("noop")))
1178            .execute(conn)
1179            .unwrap();
1180
1181        let first = conn.wal_checkpoint(None, WalCheckpointMode::Noop).unwrap();
1182        let second = conn.wal_checkpoint(None, WalCheckpointMode::Noop).unwrap();
1183
1184        assert!(!first.busy, "NOOP never blocks");
1185        assert!(first.log_frames > Some(0), "the insert sits in the WAL");
1186        assert_eq!(Some(0), first.checkpointed_frames, "nothing was moved");
1187        assert_eq!(first, second, "a second NOOP reports the same state");
1188    }
1189
1190    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
1191    #[diesel_test_helper::test]
1192    fn wal_checkpoint_reports_busy_while_a_reader_holds_an_old_snapshot() {
1193        use crate::connection::Connection;
1194
1195        let dir = tempfile::tempdir().unwrap();
1196        let path = dir.path().join("busy.db");
1197        let writer = &mut wal_connection(&path);
1198        writer.batch_execute(PROBE_TABLE).unwrap();
1199        insert_overflowing_row(writer);
1200
1201        let reader = &mut SqliteConnection::establish(path.to_str().unwrap()).unwrap();
1202        reader
1203            .transaction::<_, crate::result::Error, _>(|reader| {
1204                // Take the read snapshot, BEGIN alone defers it to the first read.
1205                let _ = pragma_probe::table.count().get_result::<i64>(reader)?;
1206
1207                // Grow the WAL past the reader's snapshot, so a blocking
1208                // checkpoint cannot complete.
1209                crate::insert_into(pragma_probe::table)
1210                    .values((pragma_probe::id.eq(2), pragma_probe::payload.eq("late")))
1211                    .execute(writer)?;
1212
1213                // Passive is never reported busy, it checkpoints up to the
1214                // reader's snapshot and leaves the rest.
1215                let outcome = writer.wal_checkpoint(None, WalCheckpointMode::Passive)?;
1216                assert!(!outcome.busy, "PASSIVE never reports busy");
1217                assert!(
1218                    outcome.checkpointed_frames < outcome.log_frames,
1219                    "the frames past the reader's snapshot stay in the WAL"
1220                );
1221
1222                for mode in [
1223                    WalCheckpointMode::Full,
1224                    WalCheckpointMode::Restart,
1225                    WalCheckpointMode::Truncate,
1226                ] {
1227                    let outcome = writer.wal_checkpoint(None, mode)?;
1228                    assert!(outcome.busy, "the open reader blocks a {mode:?} checkpoint");
1229                }
1230                Ok(())
1231            })
1232            .unwrap();
1233
1234        let outcome = writer
1235            .wal_checkpoint(None, WalCheckpointMode::Truncate)
1236            .unwrap();
1237        assert!(
1238            !outcome.busy,
1239            "the checkpoint completes once the reader is done"
1240        );
1241        assert_eq!(Some(0), outcome.log_frames);
1242    }
1243
1244    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
1245    #[diesel_test_helper::test]
1246    fn wal_checkpoint_targets_the_named_attached_database() {
1247        let dir = tempfile::tempdir().unwrap();
1248        let conn = &mut connection();
1249        conn.attach_database(dir.path().join("aux.db").to_str().unwrap(), "aux")
1250            .unwrap();
1251        conn.batch_execute("PRAGMA aux.journal_mode = WAL").unwrap();
1252        conn.batch_execute(AUX_PROBE_TABLE).unwrap();
1253        crate::insert_into(aux_pragma_probe::table)
1254            .values((
1255                aux_pragma_probe::id.eq(1),
1256                aux_pragma_probe::payload.eq("row"),
1257            ))
1258            .execute(conn)
1259            .unwrap();
1260
1261        let outcome = conn
1262            .wal_checkpoint(Some("aux"), WalCheckpointMode::Truncate)
1263            .unwrap();
1264        assert!(!outcome.busy);
1265        assert_eq!(
1266            Some(0),
1267            outcome.log_frames,
1268            "the attached database was checkpointed"
1269        );
1270
1271        // `main` is not in WAL mode, so a checkpoint naming it reports no frames.
1272        let outcome = conn
1273            .wal_checkpoint(Some("main"), WalCheckpointMode::Truncate)
1274            .unwrap();
1275        assert_eq!(None, outcome.log_frames);
1276    }
1277
1278    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
1279    #[diesel_test_helper::test]
1280    fn wal_checkpoint_unqualified_covers_every_attached_database() {
1281        let dir = tempfile::tempdir().unwrap();
1282        let conn = &mut wal_connection(&dir.path().join("main.db"));
1283        conn.batch_execute(PROBE_TABLE).unwrap();
1284        insert_overflowing_row(conn);
1285        conn.attach_database(dir.path().join("aux.db").to_str().unwrap(), "aux")
1286            .unwrap();
1287        conn.batch_execute("PRAGMA aux.journal_mode = WAL").unwrap();
1288        conn.batch_execute(AUX_PROBE_TABLE).unwrap();
1289        crate::insert_into(aux_pragma_probe::table)
1290            .values((
1291                aux_pragma_probe::id.eq(1),
1292                aux_pragma_probe::payload.eq("row"),
1293            ))
1294            .execute(conn)
1295            .unwrap();
1296
1297        conn.wal_checkpoint(None, WalCheckpointMode::Truncate)
1298            .unwrap();
1299
1300        // Both WALs are empty afterwards, which a qualified passive
1301        // checkpoint reports without moving anything.
1302        let main_after = conn
1303            .wal_checkpoint(Some("main"), WalCheckpointMode::Passive)
1304            .unwrap();
1305        assert_eq!(Some(0), main_after.log_frames, "main was checkpointed");
1306        let aux_after = conn
1307            .wal_checkpoint(Some("aux"), WalCheckpointMode::Passive)
1308            .unwrap();
1309        assert_eq!(Some(0), aux_after.log_frames, "aux was checkpointed too");
1310    }
1311
1312    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
1313    #[diesel_test_helper::test]
1314    fn wal_checkpoint_escapes_a_double_quote_in_the_schema_name() {
1315        // An unquoted identifier would be a syntax error, and the wrong quoting
1316        // would address a different database.
1317        let dir = tempfile::tempdir().unwrap();
1318        let conn = &mut connection();
1319        let schema = r#"we"ird"#;
1320        let quoted = schema.replace('"', "\"\"");
1321        conn.attach_database(dir.path().join("weird.db").to_str().unwrap(), schema)
1322            .unwrap();
1323        conn.batch_execute(&alloc::format!(r#"PRAGMA "{quoted}".journal_mode = WAL"#))
1324            .unwrap();
1325        conn.batch_execute(&alloc::format!(
1326            r#"CREATE TABLE "{quoted}".t (id INTEGER PRIMARY KEY)"#
1327        ))
1328        .unwrap();
1329
1330        let outcome = conn
1331            .wal_checkpoint(Some(schema), WalCheckpointMode::Truncate)
1332            .unwrap();
1333        assert_eq!(Some(0), outcome.log_frames, "the quoted schema was reached");
1334    }
1335
1336    #[cfg(not(miri))] // ffi string access
1337    #[diesel_test_helper::test]
1338    fn wal_checkpoint_of_an_unknown_schema_is_an_error() {
1339        let conn = &mut connection();
1340
1341        assert!(
1342            conn.wal_checkpoint(Some("nope"), WalCheckpointMode::Passive)
1343                .is_err()
1344        );
1345    }
1346
1347    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
1348    #[diesel_test_helper::test]
1349    fn wal_checkpoint_inside_a_transaction_is_an_error() {
1350        use crate::connection::Connection;
1351
1352        let dir = tempfile::tempdir().unwrap();
1353        let conn = &mut wal_connection(&dir.path().join("txn.db"));
1354        conn.batch_execute(PROBE_TABLE).unwrap();
1355
1356        let result: QueryResult<WalCheckpointOutcome> = conn.transaction(|conn| {
1357            crate::insert_into(pragma_probe::table)
1358                .values((pragma_probe::id.eq(1), pragma_probe::payload.eq("txn")))
1359                .execute(conn)?;
1360            conn.wal_checkpoint(None, WalCheckpointMode::Truncate)
1361        });
1362
1363        assert!(result.is_err(), "SQLite reports SQLITE_LOCKED");
1364    }
1365}