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::write_str(f,
            match self {
                WalCheckpointMode::Passive => "Passive",
                WalCheckpointMode::Full => "Full",
                WalCheckpointMode::Restart => "Restart",
                WalCheckpointMode::Truncate => "Truncate",
                WalCheckpointMode::Noop => "Noop",
            })
    }
}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    /// # fn main() {
280    /// #     run_test().unwrap();
281    /// # }
282    /// # fn run_test() -> QueryResult<()> {
283    /// # let dir = tempfile::tempdir().unwrap();
284    /// # let backup = dir.path().join("backup.db");
285    /// let conn = &mut SqliteConnection::establish(":memory:").unwrap();
286    /// conn.vacuum_into(None, backup.to_str().unwrap())?;
287    /// # assert!(backup.exists());
288    /// #     Ok(())
289    /// # }
290    /// ```
291    pub fn vacuum_into(&mut self, schema: Option<&str>, path: &str) -> QueryResult<()> {
292        use crate::query_dsl::RunQueryDsl;
293
294        Vacuum {
295            schema,
296            into: Some(path),
297        }
298        .execute(self)
299        .map(|_| ())
300    }
301
302    /// Checkpoint the [write-ahead log](https://www.sqlite.org/wal.html),
303    /// moving committed frames from the WAL file into the database file.
304    ///
305    /// `schema` selects one attached database by name. Unlike the other
306    /// maintenance helpers, `None` does not mean `main`: SQLite defines the
307    /// unqualified pragma to checkpoint every attached database. With
308    /// `None` and several attached databases the C API leaves the frame
309    /// counts undefined.
310    ///
311    /// A checkpoint stopped early by a reader or writer on another
312    /// connection is not an error: it sets
313    /// [`busy`](WalCheckpointOutcome::busy). On a database that is not in
314    /// WAL mode the call succeeds with both frame counts `None`, so it is
315    /// safe to issue unconditionally. Inside a transaction on its own
316    /// connection it fails with `SQLITE_LOCKED`.
317    ///
318    /// The mode argument requires SQLite 3.7.6 or later,
319    /// [`Truncate`](WalCheckpointMode::Truncate) requires 3.8.8 or later,
320    /// and [`Noop`](WalCheckpointMode::Noop) requires 3.51.0 or later.
321    /// Older versions do not report an error and treat an unrecognized
322    /// mode as [`Passive`](WalCheckpointMode::Passive).
323    ///
324    /// ```rust
325    /// # include!("../../doctest_setup.rs");
326    /// #
327    /// # fn main() {
328    /// #     run_test().unwrap();
329    /// # }
330    /// #
331    /// # fn run_test() -> QueryResult<()> {
332    /// use diesel::connection::SimpleConnection;
333    /// use diesel::sqlite::WalCheckpointMode;
334    /// # let dir = tempfile::tempdir().unwrap();
335    /// # let path = dir.path().join("app.db");
336    /// let conn = &mut SqliteConnection::establish(path.to_str().unwrap()).unwrap();
337    /// conn.batch_execute("PRAGMA journal_mode = WAL")?;
338    /// conn.batch_execute("CREATE TABLE logs (line TEXT NOT NULL)")?;
339    ///
340    /// let outcome = conn.wal_checkpoint(None, WalCheckpointMode::Truncate)?;
341    /// assert!(!outcome.busy);
342    /// // The whole WAL was moved into the database file and the log truncated.
343    /// assert_eq!(outcome.log_frames, Some(0));
344    /// assert_eq!(outcome.checkpointed_frames, Some(0));
345    /// #     Ok(())
346    /// # }
347    /// ```
348    pub fn wal_checkpoint(
349        &mut self,
350        schema: Option<&str>,
351        mode: WalCheckpointMode,
352    ) -> QueryResult<WalCheckpointOutcome> {
353        use crate::query_dsl::RunQueryDsl;
354
355        let (busy, log_frames, checkpointed_frames) =
356            WalCheckpoint { schema, mode }.get_result::<(i32, i64, i64)>(self)?;
357        Ok(WalCheckpointOutcome {
358            busy: busy != 0,
359            // On a database not in WAL mode both counts come back as -1.
360            log_frames: (log_frames >= 0).then_some(log_frames),
361            checkpointed_frames: (checkpointed_frames >= 0).then_some(checkpointed_frames),
362        })
363    }
364}
365
366// A `PRAGMA` accepts no bind parameters, neither for the schema it targets nor for the
367// value it assigns, so the schema is rendered as a quoted identifier by the query
368// builder. `name` is always a constant chosen here, never caller data.
369struct Pragma<'a, ST> {
370    schema: Option<&'a str>,
371    name: &'static str,
372    sql_type: PhantomData<ST>,
373}
374
375impl<'a, ST> Pragma<'a, ST> {
376    fn new(name: &'static str, schema: Option<&'a str>) -> Self {
377        Pragma {
378            schema,
379            name,
380            sql_type: PhantomData,
381        }
382    }
383}
384
385impl<ST> QueryFragment<Sqlite> for Pragma<'_, ST> {
386    fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Sqlite>) -> QueryResult<()> {
387        out.push_sql("PRAGMA ");
388        out.push_identifier(self.schema.unwrap_or("main"))?;
389        out.push_sql(".");
390        out.push_sql(self.name);
391        Ok(())
392    }
393}
394
395// The schema name is runtime data, so the rendered SQL is not determined by the type.
396impl<ST> QueryId for Pragma<'_, ST> {
397    type QueryId = ();
398
399    const HAS_STATIC_QUERY_ID: bool = false;
400}
401
402impl<ST> Query for Pragma<'_, ST> {
403    type SqlType = ST;
404}
405
406impl<ST> RunQueryDslSupport for Pragma<'_, ST> {}
407
408// `PRAGMA name = value` takes no bind parameter for the value either, so the integer is
409// rendered as a literal.
410struct SetPragmaInt<'a> {
411    schema: Option<&'a str>,
412    name: &'static str,
413    value: i32,
414}
415
416impl QueryFragment<Sqlite> for SetPragmaInt<'_> {
417    fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Sqlite>) -> QueryResult<()> {
418        out.push_sql("PRAGMA ");
419        out.push_identifier(self.schema.unwrap_or("main"))?;
420        out.push_sql(".");
421        out.push_sql(self.name);
422        out.push_sql(" = ");
423        out.push_sql(&self.value.to_string());
424        Ok(())
425    }
426}
427
428impl QueryId for SetPragmaInt<'_> {
429    type QueryId = ();
430
431    const HAS_STATIC_QUERY_ID: bool = false;
432}
433
434impl RunQueryDslSupport for SetPragmaInt<'_> {}
435
436// `VACUUM` names its schema as an identifier, so that operand is quoted by the query
437// builder, while the `INTO` destination is an expression and binds normally.
438struct Vacuum<'a> {
439    schema: Option<&'a str>,
440    into: Option<&'a str>,
441}
442
443impl QueryFragment<Sqlite> for Vacuum<'_> {
444    fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Sqlite>) -> QueryResult<()> {
445        out.push_sql("VACUUM ");
446        out.push_identifier(self.schema.unwrap_or("main"))?;
447        if let Some(into) = self.into {
448            out.push_sql(" INTO ");
449            out.push_bind_param::<crate::sql_types::Text, _>(into)?;
450        }
451        Ok(())
452    }
453}
454
455// The schema name is runtime data, so the rendered SQL is not determined by the type.
456impl QueryId for Vacuum<'_> {
457    type QueryId = ();
458
459    const HAS_STATIC_QUERY_ID: bool = false;
460}
461
462impl RunQueryDslSupport for Vacuum<'_> {}
463
464// Like `Pragma`, no operand can be a bind parameter. Unlike `Pragma`, a
465// `None` schema stays unqualified on purpose: the unqualified pragma
466// checkpoints every attached database, while a qualified one targets a
467// single schema. The whole checkpoint runs on the first step of the
468// statement and yields exactly one row, so a prepared statement works here
469// (unlike `incremental_vacuum`).
470struct WalCheckpoint<'a> {
471    schema: Option<&'a str>,
472    mode: WalCheckpointMode,
473}
474
475impl QueryFragment<Sqlite> for WalCheckpoint<'_> {
476    fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Sqlite>) -> QueryResult<()> {
477        out.push_sql("PRAGMA ");
478        if let Some(schema) = self.schema {
479            out.push_identifier(schema)?;
480            out.push_sql(".");
481        }
482        out.push_sql(match self.mode {
483            WalCheckpointMode::Passive => "wal_checkpoint(PASSIVE)",
484            WalCheckpointMode::Full => "wal_checkpoint(FULL)",
485            WalCheckpointMode::Restart => "wal_checkpoint(RESTART)",
486            WalCheckpointMode::Truncate => "wal_checkpoint(TRUNCATE)",
487            WalCheckpointMode::Noop => "wal_checkpoint(NOOP)",
488        });
489        Ok(())
490    }
491}
492
493// The schema name and mode are runtime data, so the rendered SQL is not determined by the type.
494impl QueryId for WalCheckpoint<'_> {
495    type QueryId = ();
496
497    const HAS_STATIC_QUERY_ID: bool = false;
498}
499
500impl Query for WalCheckpoint<'_> {
501    type SqlType = (
502        crate::sql_types::Integer,
503        crate::sql_types::BigInt,
504        crate::sql_types::BigInt,
505    );
506}
507
508impl RunQueryDslSupport for WalCheckpoint<'_> {}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513    use crate::connection::SimpleConnection;
514    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
515    use crate::dsl::sql;
516    use crate::prelude::*;
517    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
518    use crate::sql_types::{Integer, Text};
519
520    fn connection() -> SqliteConnection {
521        SqliteConnection::establish(":memory:").unwrap()
522    }
523
524    #[diesel_test_helper::test]
525    fn auto_vacuum_all_modes_roundtrip_on_fresh_database() {
526        for mode in [
527            AutoVacuumMode::None,
528            AutoVacuumMode::Full,
529            AutoVacuumMode::Incremental,
530        ] {
531            let conn = &mut connection();
532            conn.set_auto_vacuum(None, mode).unwrap();
533            assert_eq!(mode, conn.auto_vacuum(None).unwrap());
534        }
535    }
536
537    #[diesel_test_helper::test]
538    fn auto_vacuum_incremental_sticks_across_schema_creation() {
539        let conn = &mut connection();
540        conn.set_auto_vacuum(None, AutoVacuumMode::Incremental)
541            .unwrap();
542        assert_eq!(AutoVacuumMode::Incremental, conn.auto_vacuum(None).unwrap());
543
544        crate::sql_query("CREATE TABLE t (id INTEGER PRIMARY KEY)")
545            .execute(conn)
546            .unwrap();
547        assert_eq!(
548            AutoVacuumMode::Incremental,
549            conn.auto_vacuum(None).unwrap(),
550            "the mode survives once the schema exists"
551        );
552    }
553
554    #[diesel_test_helper::test]
555    fn auto_vacuum_change_from_none_requires_vacuum_on_populated_database() {
556        let conn = &mut connection();
557        crate::sql_query("CREATE TABLE t (id INTEGER PRIMARY KEY)")
558            .execute(conn)
559            .unwrap();
560        crate::sql_query("INSERT INTO t (id) VALUES (1)")
561            .execute(conn)
562            .unwrap();
563        assert_eq!(AutoVacuumMode::None, conn.auto_vacuum(None).unwrap());
564
565        // On a populated database the switch away from `None` is silently
566        // deferred until a full rewrite.
567        conn.set_auto_vacuum(None, AutoVacuumMode::Full).unwrap();
568        assert_eq!(
569            AutoVacuumMode::None,
570            conn.auto_vacuum(None).unwrap(),
571            "the change does not take effect without a VACUUM"
572        );
573
574        crate::sql_query("VACUUM").execute(conn).unwrap();
575        assert_eq!(
576            AutoVacuumMode::Full,
577            conn.auto_vacuum(None).unwrap(),
578            "VACUUM rewrites the file and applies the mode"
579        );
580    }
581
582    #[diesel_test_helper::test]
583    fn auto_vacuum_targets_the_named_attached_database() {
584        let conn = &mut connection();
585        crate::sql_query("ATTACH DATABASE ':memory:' AS aux")
586            .execute(conn)
587            .unwrap();
588
589        conn.set_auto_vacuum(Some("aux"), AutoVacuumMode::Full)
590            .unwrap();
591        assert_eq!(AutoVacuumMode::Full, conn.auto_vacuum(Some("aux")).unwrap());
592        assert_eq!(
593            AutoVacuumMode::None,
594            conn.auto_vacuum(None).unwrap(),
595            "main keeps its own default"
596        );
597    }
598
599    #[diesel_test_helper::test]
600    fn auto_vacuum_schema_name_with_double_quote_is_handled() {
601        let conn = &mut connection();
602        let schema = r#"we"ird"#;
603        crate::sql_query(alloc::format!(
604            r#"ATTACH DATABASE ':memory:' AS "{}""#,
605            schema.replace('"', "\"\"")
606        ))
607        .execute(conn)
608        .unwrap();
609
610        conn.set_auto_vacuum(Some(schema), AutoVacuumMode::Incremental)
611            .unwrap();
612        assert_eq!(
613            AutoVacuumMode::Incremental,
614            conn.auto_vacuum(Some(schema)).unwrap()
615        );
616    }
617
618    table! {
619        pragma_probe (id) {
620            id -> Integer,
621            payload -> Text,
622        }
623    }
624
625    table! {
626        aux.aux_pragma_probe (id) {
627            id -> Integer,
628            payload -> Text,
629        }
630    }
631
632    const PROBE_TABLE: &str =
633        "CREATE TABLE pragma_probe (id INTEGER PRIMARY KEY, payload TEXT NOT NULL)";
634
635    const AUX_PROBE_TABLE: &str =
636        "CREATE TABLE aux.aux_pragma_probe (id INTEGER PRIMARY KEY, payload TEXT NOT NULL)";
637
638    // Large enough to spill onto overflow pages, so the database outgrows a single page
639    // and leaves reclaimable pages behind once the row is deleted.
640    fn overflowing_payload() -> String {
641        "x".repeat(64 * 1024)
642    }
643
644    fn insert_overflowing_row(conn: &mut SqliteConnection) {
645        crate::insert_into(pragma_probe::table)
646            .values((
647                pragma_probe::id.eq(1),
648                pragma_probe::payload.eq(overflowing_payload()),
649            ))
650            .execute(conn)
651            .unwrap();
652    }
653
654    #[diesel_test_helper::test]
655    fn page_count_is_positive_and_grows() {
656        let conn = &mut connection();
657        conn.batch_execute(PROBE_TABLE).unwrap();
658        let initial = conn.page_count(None).unwrap();
659        assert!(initial > 0, "an initialized database has at least one page");
660
661        insert_overflowing_row(conn);
662
663        assert!(
664            conn.page_count(None).unwrap() > initial,
665            "a row spanning overflow pages grows the page count"
666        );
667    }
668
669    #[diesel_test_helper::test]
670    fn freelist_count_tracks_reclaimable_space() {
671        let conn = &mut connection();
672        assert_eq!(
673            0,
674            conn.freelist_count(None).unwrap(),
675            "a fresh database has an empty freelist"
676        );
677
678        conn.batch_execute(PROBE_TABLE).unwrap();
679        insert_overflowing_row(conn);
680
681        crate::delete(pragma_probe::table).execute(conn).unwrap();
682        assert!(
683            conn.freelist_count(None).unwrap() > 0,
684            "deleting the row leaves reclaimable pages on the freelist"
685        );
686
687        // `VACUUM` has no query DSL equivalent.
688        crate::sql_query("VACUUM").execute(conn).unwrap();
689        assert_eq!(
690            0,
691            conn.freelist_count(None).unwrap(),
692            "VACUUM reclaims the freelist"
693        );
694    }
695
696    #[diesel_test_helper::test]
697    fn schema_targets_the_named_attached_database() {
698        let conn = &mut connection();
699        conn.batch_execute(PROBE_TABLE).unwrap();
700        conn.attach_database(":memory:", "aux").unwrap();
701        conn.batch_execute(AUX_PROBE_TABLE).unwrap();
702        crate::insert_into(aux_pragma_probe::table)
703            .values((
704                aux_pragma_probe::id.eq(1),
705                aux_pragma_probe::payload.eq(overflowing_payload()),
706            ))
707            .execute(conn)
708            .unwrap();
709
710        let main_pages = conn.page_count(None).unwrap();
711        let aux_pages = conn.page_count(Some("aux")).unwrap();
712        assert!(
713            aux_pages > main_pages,
714            "the attached database holds the data, main stays small"
715        );
716        assert_eq!(
717            main_pages,
718            conn.page_count(Some("main")).unwrap(),
719            "an explicit main matches the default"
720        );
721    }
722
723    #[diesel_test_helper::test]
724    fn schema_name_with_backtick_is_escaped() {
725        // The query builder quotes SQLite identifiers with backticks, so a backtick is
726        // the character that has to be doubled.
727        let conn = &mut connection();
728        let schema = "back`tick";
729        conn.attach_database(":memory:", schema).unwrap();
730        conn.batch_execute("CREATE TABLE `back``tick`.probe (id INTEGER PRIMARY KEY)")
731            .unwrap();
732
733        assert!(conn.page_count(Some(schema)).unwrap() > 0);
734        assert_eq!(0, conn.freelist_count(Some(schema)).unwrap());
735    }
736
737    #[cfg(not(miri))] // ffi string access
738    #[diesel_test_helper::test]
739    fn unknown_schema_is_reported_as_an_error() {
740        let conn = &mut connection();
741
742        assert!(conn.page_count(Some("nope")).is_err());
743        assert!(conn.freelist_count(Some("nope")).is_err());
744    }
745
746    // Leaves many pages on the freelist, so `incremental_vacuum` has something to
747    // reclaim and a bound smaller than the freelist is meaningful.
748    fn grow_then_empty_freelist(conn: &mut SqliteConnection) {
749        conn.batch_execute(PROBE_TABLE).unwrap();
750        let rows = (1..=200)
751            .map(|id| {
752                (
753                    pragma_probe::id.eq(id),
754                    pragma_probe::payload.eq("x".repeat(4000)),
755                )
756            })
757            .collect::<Vec<_>>();
758        crate::insert_into(pragma_probe::table)
759            .values(rows)
760            .execute(conn)
761            .unwrap();
762        crate::delete(pragma_probe::table).execute(conn).unwrap();
763    }
764
765    // The same, in an attached schema.
766    fn grow_then_empty_aux_freelist(conn: &mut SqliteConnection) {
767        conn.batch_execute(AUX_PROBE_TABLE).unwrap();
768        let rows = (1..=200)
769            .map(|id| {
770                (
771                    aux_pragma_probe::id.eq(id),
772                    aux_pragma_probe::payload.eq("x".repeat(4000)),
773                )
774            })
775            .collect::<Vec<_>>();
776        crate::insert_into(aux_pragma_probe::table)
777            .values(rows)
778            .execute(conn)
779            .unwrap();
780        crate::delete(aux_pragma_probe::table)
781            .execute(conn)
782            .unwrap();
783    }
784
785    #[diesel_test_helper::test]
786    fn incremental_vacuum_clears_the_whole_freelist() {
787        let conn = &mut connection();
788        conn.set_auto_vacuum(None, AutoVacuumMode::Incremental)
789            .unwrap();
790        grow_then_empty_freelist(conn);
791        assert!(
792            conn.freelist_count(None).unwrap() > 1,
793            "the deleted rows should leave many pages on the freelist"
794        );
795
796        conn.incremental_vacuum(None, None).unwrap();
797
798        // Stepping the pragma only once would free a single page and leave the rest, so
799        // this also pins that the statement is driven to completion.
800        assert_eq!(0, conn.freelist_count(None).unwrap());
801    }
802
803    #[diesel_test_helper::test]
804    fn incremental_vacuum_reclaims_at_most_the_requested_pages() {
805        let conn = &mut connection();
806        conn.set_auto_vacuum(None, AutoVacuumMode::Incremental)
807            .unwrap();
808        grow_then_empty_freelist(conn);
809        let before = conn.freelist_count(None).unwrap();
810        assert!(before > 10, "the bound has to be smaller than the freelist");
811
812        conn.incremental_vacuum(None, Some(10)).unwrap();
813
814        let after = conn.freelist_count(None).unwrap();
815        assert!(after >= before - 10, "at most ten pages may be reclaimed");
816        assert!(after < before, "some pages should have been reclaimed");
817    }
818
819    #[diesel_test_helper::test]
820    fn incremental_vacuum_is_a_no_op_outside_incremental_mode() {
821        let conn = &mut connection();
822        assert_eq!(AutoVacuumMode::None, conn.auto_vacuum(None).unwrap());
823        grow_then_empty_freelist(conn);
824        let before = conn.freelist_count(None).unwrap();
825        assert!(before > 0);
826
827        conn.incremental_vacuum(None, None).unwrap();
828
829        assert_eq!(
830            before,
831            conn.freelist_count(None).unwrap(),
832            "a database that is not in incremental mode keeps its freelist"
833        );
834    }
835
836    #[diesel_test_helper::test]
837    fn incremental_vacuum_targets_the_named_attached_database() {
838        let conn = &mut connection();
839        conn.attach_database(":memory:", "aux").unwrap();
840        conn.set_auto_vacuum(Some("aux"), AutoVacuumMode::Incremental)
841            .unwrap();
842
843        grow_then_empty_aux_freelist(conn);
844        assert!(conn.freelist_count(Some("aux")).unwrap() > 0);
845
846        conn.incremental_vacuum(Some("aux"), None).unwrap();
847
848        assert_eq!(0, conn.freelist_count(Some("aux")).unwrap());
849    }
850
851    #[diesel_test_helper::test]
852    fn incremental_vacuum_escapes_a_backtick_in_the_schema_name() {
853        // An unquoted identifier would be a syntax error, and the wrong quoting would
854        // address a different database.
855        let conn = &mut connection();
856        let schema = "back`tick";
857        conn.attach_database(":memory:", schema).unwrap();
858
859        conn.incremental_vacuum(Some(schema), None).unwrap();
860
861        assert_eq!(0, conn.freelist_count(Some(schema)).unwrap());
862    }
863
864    #[diesel_test_helper::test]
865    fn incremental_vacuum_of_zero_pages_clears_everything() {
866        // SQLite specifies that a bound below one clears the whole freelist.
867        let conn = &mut connection();
868        conn.set_auto_vacuum(None, AutoVacuumMode::Incremental)
869            .unwrap();
870        grow_then_empty_freelist(conn);
871        assert!(conn.freelist_count(None).unwrap() > 0);
872
873        conn.incremental_vacuum(None, Some(0)).unwrap();
874
875        assert_eq!(0, conn.freelist_count(None).unwrap());
876    }
877
878    #[cfg(not(miri))] // ffi string access
879    #[diesel_test_helper::test]
880    fn incremental_vacuum_of_an_unknown_schema_is_an_error() {
881        let conn = &mut connection();
882
883        assert!(conn.incremental_vacuum(Some("nope"), None).is_err());
884    }
885
886    // Leaves the database holding one small row but occupying many pages, so a rebuild
887    // has something to reclaim.
888    fn fill_then_delete(conn: &mut SqliteConnection) {
889        conn.batch_execute(PROBE_TABLE).unwrap();
890        crate::insert_into(pragma_probe::table)
891            .values((
892                pragma_probe::id.eq(1),
893                pragma_probe::payload.eq("x".repeat(256 * 1024)),
894            ))
895            .execute(conn)
896            .unwrap();
897        crate::delete(pragma_probe::table).execute(conn).unwrap();
898        crate::insert_into(pragma_probe::table)
899            .values((pragma_probe::id.eq(2), pragma_probe::payload.eq("kept")))
900            .execute(conn)
901            .unwrap();
902    }
903
904    // The same, in an attached schema.
905    fn fill_then_delete_aux(conn: &mut SqliteConnection) {
906        conn.batch_execute(AUX_PROBE_TABLE).unwrap();
907        crate::insert_into(aux_pragma_probe::table)
908            .values((
909                aux_pragma_probe::id.eq(1),
910                aux_pragma_probe::payload.eq("x".repeat(256 * 1024)),
911            ))
912            .execute(conn)
913            .unwrap();
914        crate::delete(aux_pragma_probe::table)
915            .execute(conn)
916            .unwrap();
917    }
918
919    #[diesel_test_helper::test]
920    fn vacuum_repacks_the_database() {
921        let conn = &mut connection();
922        fill_then_delete(conn);
923        let before = conn.page_count(None).unwrap();
924        assert!(before > 1);
925
926        conn.vacuum(None).unwrap();
927
928        assert!(
929            conn.page_count(None).unwrap() < before,
930            "rebuilding should release the pages the deleted row occupied"
931        );
932        assert_eq!(
933            1,
934            pragma_probe::table.count().get_result::<i64>(conn).unwrap(),
935            "the surviving row is still there"
936        );
937    }
938
939    #[diesel_test_helper::test]
940    fn vacuum_targets_the_named_attached_database() {
941        let conn = &mut connection();
942        conn.attach_database(":memory:", "aux").unwrap();
943        fill_then_delete_aux(conn);
944        let before = conn.page_count(Some("aux")).unwrap();
945        assert!(before > 1);
946
947        conn.vacuum(Some("aux")).unwrap();
948
949        assert!(conn.page_count(Some("aux")).unwrap() < before);
950    }
951
952    #[cfg(not(miri))]
953    #[diesel_test_helper::test]
954    fn vacuum_inside_a_transaction_is_an_error() {
955        use crate::connection::Connection;
956
957        let conn = &mut connection();
958        let result: QueryResult<()> = conn.transaction(|conn| conn.vacuum(None));
959
960        assert!(result.is_err());
961    }
962
963    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
964    #[diesel_test_helper::test]
965    fn vacuum_into_writes_a_readable_copy_through_a_quoted_path() {
966        let dir = tempfile::tempdir().unwrap();
967        // A quote in the path would break a hand-assembled statement. It is a bind
968        // parameter, so it is taken verbatim.
969        let destination = dir.path().join("o'brien backup.db");
970
971        let conn = &mut connection();
972        conn.batch_execute(PROBE_TABLE).unwrap();
973        crate::insert_into(pragma_probe::table)
974            .values((pragma_probe::id.eq(1), pragma_probe::payload.eq("copied")))
975            .execute(conn)
976            .unwrap();
977
978        conn.vacuum_into(None, destination.to_str().unwrap())
979            .unwrap();
980
981        let copy = &mut SqliteConnection::establish(destination.to_str().unwrap()).unwrap();
982        assert_eq!(
983            "copied",
984            pragma_probe::table
985                .select(pragma_probe::payload)
986                .get_result::<String>(copy)
987                .unwrap()
988        );
989    }
990
991    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
992    #[diesel_test_helper::test]
993    fn vacuum_into_refuses_to_overwrite_an_existing_database() {
994        let dir = tempfile::tempdir().unwrap();
995        let destination = dir.path().join("occupied.db");
996        {
997            let occupied = &mut SqliteConnection::establish(destination.to_str().unwrap()).unwrap();
998            occupied.batch_execute(PROBE_TABLE).unwrap();
999        }
1000
1001        let conn = &mut connection();
1002        conn.batch_execute(PROBE_TABLE).unwrap();
1003
1004        assert!(
1005            conn.vacuum_into(None, destination.to_str().unwrap())
1006                .is_err()
1007        );
1008    }
1009
1010    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
1011    #[diesel_test_helper::test]
1012    fn vacuum_into_copies_the_named_attached_database() {
1013        let dir = tempfile::tempdir().unwrap();
1014        let destination = dir.path().join("aux copy.db");
1015
1016        let conn = &mut connection();
1017        conn.attach_database(":memory:", "aux").unwrap();
1018        conn.batch_execute(AUX_PROBE_TABLE).unwrap();
1019        crate::insert_into(aux_pragma_probe::table)
1020            .values((
1021                aux_pragma_probe::id.eq(7),
1022                aux_pragma_probe::payload.eq("copied"),
1023            ))
1024            .execute(conn)
1025            .unwrap();
1026
1027        conn.vacuum_into(Some("aux"), destination.to_str().unwrap())
1028            .unwrap();
1029
1030        let copy = &mut SqliteConnection::establish(destination.to_str().unwrap()).unwrap();
1031        // In the copy the table sits in `main`, while `aux_pragma_probe` is declared
1032        // schema-qualified, so this one read cannot go through it.
1033        let id = sql::<Integer>("SELECT id FROM aux_pragma_probe")
1034            .get_result::<i32>(copy)
1035            .unwrap();
1036        assert_eq!(7, id);
1037    }
1038
1039    #[diesel_test_helper::test]
1040    fn vacuum_escapes_a_backtick_in_the_schema_name() {
1041        let conn = &mut connection();
1042        let schema = "back`tick";
1043        conn.attach_database(":memory:", schema).unwrap();
1044
1045        conn.vacuum(Some(schema)).unwrap();
1046    }
1047
1048    #[diesel_test_helper::test]
1049    fn vacuuming_two_schemas_rebuilds_each_of_them() {
1050        // The schema is part of the rendered SQL, so the two calls must not share a
1051        // prepared statement. If they did, the second would rebuild the first's
1052        // database again and leave this one untouched.
1053        let conn = &mut connection();
1054        fill_then_delete(conn);
1055        conn.attach_database(":memory:", "aux").unwrap();
1056        fill_then_delete_aux(conn);
1057
1058        let main_before = conn.page_count(None).unwrap();
1059        let aux_before = conn.page_count(Some("aux")).unwrap();
1060
1061        conn.vacuum(None).unwrap();
1062        conn.vacuum(Some("aux")).unwrap();
1063
1064        assert!(
1065            conn.page_count(None).unwrap() < main_before,
1066            "main was rebuilt"
1067        );
1068        assert!(
1069            conn.page_count(Some("aux")).unwrap() < aux_before,
1070            "aux was rebuilt too, not main a second time"
1071        );
1072    }
1073
1074    // WAL requires a real file.
1075    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
1076    fn wal_connection(path: &std::path::Path) -> SqliteConnection {
1077        let mut conn = SqliteConnection::establish(path.to_str().unwrap()).unwrap();
1078        conn.batch_execute("PRAGMA journal_mode = WAL").unwrap();
1079        conn
1080    }
1081
1082    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
1083    #[diesel_test_helper::test]
1084    fn wal_checkpoint_truncate_reports_an_emptied_wal() {
1085        let dir = tempfile::tempdir().unwrap();
1086        let conn = &mut wal_connection(&dir.path().join("wal.db"));
1087        conn.batch_execute(PROBE_TABLE).unwrap();
1088        insert_overflowing_row(conn);
1089
1090        let outcome = conn
1091            .wal_checkpoint(None, WalCheckpointMode::Truncate)
1092            .unwrap();
1093
1094        assert!(!outcome.busy);
1095        assert_eq!(Some(0), outcome.log_frames, "the WAL file was truncated");
1096        assert_eq!(Some(0), outcome.checkpointed_frames);
1097    }
1098
1099    #[diesel_test_helper::test]
1100    fn wal_checkpoint_outside_wal_mode_reports_no_frames() {
1101        let conn = &mut connection();
1102
1103        let outcome = conn
1104            .wal_checkpoint(None, WalCheckpointMode::Truncate)
1105            .unwrap();
1106
1107        assert!(!outcome.busy);
1108        assert_eq!(None, outcome.log_frames);
1109        assert_eq!(None, outcome.checkpointed_frames);
1110    }
1111
1112    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
1113    #[diesel_test_helper::test]
1114    fn wal_checkpoint_accepts_every_mode() {
1115        let dir = tempfile::tempdir().unwrap();
1116        let conn = &mut wal_connection(&dir.path().join("modes.db"));
1117        conn.batch_execute(PROBE_TABLE).unwrap();
1118
1119        for (row, mode) in [
1120            WalCheckpointMode::Passive,
1121            WalCheckpointMode::Full,
1122            WalCheckpointMode::Restart,
1123            WalCheckpointMode::Truncate,
1124            WalCheckpointMode::Noop,
1125        ]
1126        .into_iter()
1127        .enumerate()
1128        {
1129            // A fresh row per round gives every mode frames to move.
1130            crate::insert_into(pragma_probe::table)
1131                .values((
1132                    pragma_probe::id.eq(i32::try_from(row).unwrap() + 1),
1133                    pragma_probe::payload.eq("row"),
1134                ))
1135                .execute(conn)
1136                .unwrap();
1137
1138            let outcome = conn.wal_checkpoint(None, mode).unwrap();
1139            assert!(!outcome.busy, "{mode:?} had no competing readers");
1140            assert!(
1141                outcome.log_frames.is_some(),
1142                "{mode:?} ran on a WAL database"
1143            );
1144            assert!(outcome.checkpointed_frames.is_some());
1145            assert!(
1146                outcome.checkpointed_frames <= outcome.log_frames,
1147                "{mode:?}: checkpointed frames cannot exceed the log size"
1148            );
1149        }
1150    }
1151
1152    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
1153    #[diesel_test_helper::test]
1154    fn wal_checkpoint_noop_reports_state_without_moving_frames() {
1155        let dir = tempfile::tempdir().unwrap();
1156        let conn = &mut wal_connection(&dir.path().join("noop.db"));
1157
1158        // NOOP exists since SQLite 3.51.0, older versions run PASSIVE instead.
1159        let version = crate::select(sql::<Text>("sqlite_version()"))
1160            .get_result::<String>(conn)
1161            .unwrap();
1162        let mut parts = version.split('.').map(|part| part.parse::<u32>().unwrap());
1163        if (parts.next().unwrap(), parts.next().unwrap()) < (3, 51) {
1164            return;
1165        }
1166
1167        conn.batch_execute(PROBE_TABLE).unwrap();
1168        conn.wal_checkpoint(None, WalCheckpointMode::Truncate)
1169            .unwrap();
1170        crate::insert_into(pragma_probe::table)
1171            .values((pragma_probe::id.eq(1), pragma_probe::payload.eq("noop")))
1172            .execute(conn)
1173            .unwrap();
1174
1175        let first = conn.wal_checkpoint(None, WalCheckpointMode::Noop).unwrap();
1176        let second = conn.wal_checkpoint(None, WalCheckpointMode::Noop).unwrap();
1177
1178        assert!(!first.busy, "NOOP never blocks");
1179        assert!(first.log_frames > Some(0), "the insert sits in the WAL");
1180        assert_eq!(Some(0), first.checkpointed_frames, "nothing was moved");
1181        assert_eq!(first, second, "a second NOOP reports the same state");
1182    }
1183
1184    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
1185    #[diesel_test_helper::test]
1186    fn wal_checkpoint_reports_busy_while_a_reader_holds_an_old_snapshot() {
1187        use crate::connection::Connection;
1188
1189        let dir = tempfile::tempdir().unwrap();
1190        let path = dir.path().join("busy.db");
1191        let writer = &mut wal_connection(&path);
1192        writer.batch_execute(PROBE_TABLE).unwrap();
1193        insert_overflowing_row(writer);
1194
1195        let reader = &mut SqliteConnection::establish(path.to_str().unwrap()).unwrap();
1196        reader
1197            .transaction::<_, crate::result::Error, _>(|reader| {
1198                // Take the read snapshot, BEGIN alone defers it to the first read.
1199                let _ = pragma_probe::table.count().get_result::<i64>(reader)?;
1200
1201                // Grow the WAL past the reader's snapshot, so a blocking
1202                // checkpoint cannot complete.
1203                crate::insert_into(pragma_probe::table)
1204                    .values((pragma_probe::id.eq(2), pragma_probe::payload.eq("late")))
1205                    .execute(writer)?;
1206
1207                // Passive is never reported busy, it checkpoints up to the
1208                // reader's snapshot and leaves the rest.
1209                let outcome = writer.wal_checkpoint(None, WalCheckpointMode::Passive)?;
1210                assert!(!outcome.busy, "PASSIVE never reports busy");
1211                assert!(
1212                    outcome.checkpointed_frames < outcome.log_frames,
1213                    "the frames past the reader's snapshot stay in the WAL"
1214                );
1215
1216                for mode in [
1217                    WalCheckpointMode::Full,
1218                    WalCheckpointMode::Restart,
1219                    WalCheckpointMode::Truncate,
1220                ] {
1221                    let outcome = writer.wal_checkpoint(None, mode)?;
1222                    assert!(outcome.busy, "the open reader blocks a {mode:?} checkpoint");
1223                }
1224                Ok(())
1225            })
1226            .unwrap();
1227
1228        let outcome = writer
1229            .wal_checkpoint(None, WalCheckpointMode::Truncate)
1230            .unwrap();
1231        assert!(
1232            !outcome.busy,
1233            "the checkpoint completes once the reader is done"
1234        );
1235        assert_eq!(Some(0), outcome.log_frames);
1236    }
1237
1238    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
1239    #[diesel_test_helper::test]
1240    fn wal_checkpoint_targets_the_named_attached_database() {
1241        let dir = tempfile::tempdir().unwrap();
1242        let conn = &mut connection();
1243        conn.attach_database(dir.path().join("aux.db").to_str().unwrap(), "aux")
1244            .unwrap();
1245        conn.batch_execute("PRAGMA aux.journal_mode = WAL").unwrap();
1246        conn.batch_execute(AUX_PROBE_TABLE).unwrap();
1247        crate::insert_into(aux_pragma_probe::table)
1248            .values((
1249                aux_pragma_probe::id.eq(1),
1250                aux_pragma_probe::payload.eq("row"),
1251            ))
1252            .execute(conn)
1253            .unwrap();
1254
1255        let outcome = conn
1256            .wal_checkpoint(Some("aux"), WalCheckpointMode::Truncate)
1257            .unwrap();
1258        assert!(!outcome.busy);
1259        assert_eq!(
1260            Some(0),
1261            outcome.log_frames,
1262            "the attached database was checkpointed"
1263        );
1264
1265        // `main` is not in WAL mode, so a checkpoint naming it reports no frames.
1266        let outcome = conn
1267            .wal_checkpoint(Some("main"), WalCheckpointMode::Truncate)
1268            .unwrap();
1269        assert_eq!(None, outcome.log_frames);
1270    }
1271
1272    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
1273    #[diesel_test_helper::test]
1274    fn wal_checkpoint_unqualified_covers_every_attached_database() {
1275        let dir = tempfile::tempdir().unwrap();
1276        let conn = &mut wal_connection(&dir.path().join("main.db"));
1277        conn.batch_execute(PROBE_TABLE).unwrap();
1278        insert_overflowing_row(conn);
1279        conn.attach_database(dir.path().join("aux.db").to_str().unwrap(), "aux")
1280            .unwrap();
1281        conn.batch_execute("PRAGMA aux.journal_mode = WAL").unwrap();
1282        conn.batch_execute(AUX_PROBE_TABLE).unwrap();
1283        crate::insert_into(aux_pragma_probe::table)
1284            .values((
1285                aux_pragma_probe::id.eq(1),
1286                aux_pragma_probe::payload.eq("row"),
1287            ))
1288            .execute(conn)
1289            .unwrap();
1290
1291        conn.wal_checkpoint(None, WalCheckpointMode::Truncate)
1292            .unwrap();
1293
1294        // Both WALs are empty afterwards, which a qualified passive
1295        // checkpoint reports without moving anything.
1296        let main_after = conn
1297            .wal_checkpoint(Some("main"), WalCheckpointMode::Passive)
1298            .unwrap();
1299        assert_eq!(Some(0), main_after.log_frames, "main was checkpointed");
1300        let aux_after = conn
1301            .wal_checkpoint(Some("aux"), WalCheckpointMode::Passive)
1302            .unwrap();
1303        assert_eq!(Some(0), aux_after.log_frames, "aux was checkpointed too");
1304    }
1305
1306    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
1307    #[diesel_test_helper::test]
1308    fn wal_checkpoint_escapes_a_double_quote_in_the_schema_name() {
1309        // An unquoted identifier would be a syntax error, and the wrong quoting
1310        // would address a different database.
1311        let dir = tempfile::tempdir().unwrap();
1312        let conn = &mut connection();
1313        let schema = r#"we"ird"#;
1314        let quoted = schema.replace('"', "\"\"");
1315        conn.attach_database(dir.path().join("weird.db").to_str().unwrap(), schema)
1316            .unwrap();
1317        conn.batch_execute(&alloc::format!(r#"PRAGMA "{quoted}".journal_mode = WAL"#))
1318            .unwrap();
1319        conn.batch_execute(&alloc::format!(
1320            r#"CREATE TABLE "{quoted}".t (id INTEGER PRIMARY KEY)"#
1321        ))
1322        .unwrap();
1323
1324        let outcome = conn
1325            .wal_checkpoint(Some(schema), WalCheckpointMode::Truncate)
1326            .unwrap();
1327        assert_eq!(Some(0), outcome.log_frames, "the quoted schema was reached");
1328    }
1329
1330    #[cfg(not(miri))] // ffi string access
1331    #[diesel_test_helper::test]
1332    fn wal_checkpoint_of_an_unknown_schema_is_an_error() {
1333        let conn = &mut connection();
1334
1335        assert!(
1336            conn.wal_checkpoint(Some("nope"), WalCheckpointMode::Passive)
1337                .is_err()
1338        );
1339    }
1340
1341    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
1342    #[diesel_test_helper::test]
1343    fn wal_checkpoint_inside_a_transaction_is_an_error() {
1344        use crate::connection::Connection;
1345
1346        let dir = tempfile::tempdir().unwrap();
1347        let conn = &mut wal_connection(&dir.path().join("txn.db"));
1348        conn.batch_execute(PROBE_TABLE).unwrap();
1349
1350        let result: QueryResult<WalCheckpointOutcome> = conn.transaction(|conn| {
1351            crate::insert_into(pragma_probe::table)
1352                .values((pragma_probe::id.eq(1), pragma_probe::payload.eq("txn")))
1353                .execute(conn)?;
1354            conn.wal_checkpoint(None, WalCheckpointMode::Truncate)
1355        });
1356
1357        assert!(result.is_err(), "SQLite reports SQLITE_LOCKED");
1358    }
1359}