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#[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 None = 0,
26 Full = 1,
28 Incremental = 2,
31}
32
33#[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 Passive,
41 Full,
44 Restart,
47 Truncate,
50 Noop,
56}
57
58#[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 pub busy: bool,
66 pub log_frames: Option<i64>,
69 pub checkpointed_frames: Option<i64>,
74}
75
76impl SqliteConnection {
77 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 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 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 log_frames: (log_frames >= 0).then_some(log_frames),
211 checkpointed_frames: (checkpointed_frames >= 0).then_some(checkpointed_frames),
212 })
213 }
214}
215
216struct 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
235impl QueryId for Vacuum<'_> {
237 type QueryId = ();
238
239 const HAS_STATIC_QUERY_ID: bool = false;
240}
241
242impl RunQueryDslSupport for Vacuum<'_> {}
243
244struct 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
273impl 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 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 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 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 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))] #[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 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 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 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 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))] #[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 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 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 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 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 #[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 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 #[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 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 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 let _ = pragma_probe::table.count().get_result::<i64>(reader)?;
989
990 crate::insert_into(pragma_probe::table)
993 .values((pragma_probe::id.eq(2), pragma_probe::payload.eq("late")))
994 .execute(writer)?;
995
996 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 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 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 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))] #[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}