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