use super::SqliteConnection;
use crate::query_builder::{AstPass, QueryFragment, QueryId};
use crate::query_dsl::RunQueryDslSupport;
use crate::result::QueryResult;
use crate::sqlite::Sqlite;
use alloc::ffi::CString;
impl SqliteConnection {
pub fn attach_database(&mut self, path: &str, schema_name: &str) -> QueryResult<()> {
use crate::query_dsl::RunQueryDsl;
CString::new(path)?;
CString::new(schema_name)?;
AttachDatabase { path, schema_name }
.execute(self)
.map(|_| ())
}
pub fn detach_database(&mut self, schema_name: &str) -> QueryResult<()> {
use crate::query_dsl::RunQueryDsl;
CString::new(schema_name)?;
DetachDatabase { schema_name }.execute(self).map(|_| ())
}
}
#[derive(QueryId)]
struct AttachDatabase<'a> {
path: &'a str,
schema_name: &'a str,
}
impl QueryFragment<Sqlite> for AttachDatabase<'_> {
fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Sqlite>) -> QueryResult<()> {
out.push_sql("ATTACH DATABASE ");
out.push_bind_param::<crate::sql_types::Text, _>(self.path)?;
out.push_sql(" AS ");
out.push_bind_param::<crate::sql_types::Text, _>(self.schema_name)?;
Ok(())
}
}
impl RunQueryDslSupport for AttachDatabase<'_> {}
#[derive(QueryId)]
struct DetachDatabase<'a> {
schema_name: &'a str,
}
impl QueryFragment<Sqlite> for DetachDatabase<'_> {
fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Sqlite>) -> QueryResult<()> {
out.push_sql("DETACH DATABASE ");
out.push_bind_param::<crate::sql_types::Text, _>(self.schema_name)?;
Ok(())
}
}
impl RunQueryDslSupport for DetachDatabase<'_> {}
#[cfg(test)]
mod tests {
use super::*;
use crate::dsl::sql;
use crate::prelude::*;
use crate::result::Error;
use crate::sql_types::Integer;
fn connection() -> SqliteConnection {
SqliteConnection::establish(":memory:").unwrap()
}
#[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
fn temp_db_path(name: &str) -> (tempfile::TempDir, std::path::PathBuf) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(name);
(dir, path)
}
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
table! {
attach_owners (id) {
id -> Integer,
name -> Text,
}
}
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
table! {
aux.attach_pets (id) {
id -> Integer,
owner_id -> Integer,
name -> Text,
}
}
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
allow_tables_to_appear_in_same_query!(attach_owners, attach_pets);
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
table! {
attach_marker (id) {
id -> Integer,
}
}
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
table! {
ro.readonly_marker (id) {
id -> Integer,
}
}
#[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
#[diesel_test_helper::test]
fn attach_database_supports_cross_schema_join_then_detach() {
use crate::connection::SimpleConnection;
let conn = &mut connection();
conn.attach_database(":memory:", "aux").unwrap();
conn.batch_execute(
"CREATE TABLE attach_owners (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
CREATE TABLE aux.attach_pets (id INTEGER PRIMARY KEY, owner_id INTEGER, name TEXT NOT NULL);",
)
.unwrap();
crate::insert_into(attach_owners::table)
.values(&[
(attach_owners::id.eq(1), attach_owners::name.eq("Sean")),
(attach_owners::id.eq(2), attach_owners::name.eq("Tess")),
])
.execute(conn)
.unwrap();
crate::insert_into(attach_pets::table)
.values((
attach_pets::id.eq(1),
attach_pets::owner_id.eq(1),
attach_pets::name.eq("Ferris"),
))
.execute(conn)
.unwrap();
let pet_owner = attach_owners::table
.inner_join(attach_pets::table.on(attach_pets::owner_id.eq(attach_owners::id)))
.filter(attach_pets::name.eq("Ferris"))
.select(attach_owners::name)
.get_result::<String>(conn)
.unwrap();
assert_eq!(pet_owner, "Sean");
conn.detach_database("aux").unwrap();
assert!(
attach_pets::table
.select(attach_pets::name)
.get_result::<String>(conn)
.is_err()
);
}
#[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
#[diesel_test_helper::test]
fn attach_database_binds_path_verbatim_without_quoting() {
let (_dir, path) = temp_db_path("o'brien.db");
conn_attach_roundtrip(&path);
let mut direct = SqliteConnection::establish(path.to_str().unwrap()).unwrap();
let count = attach_marker::table
.count()
.get_result::<i64>(&mut direct)
.unwrap();
assert_eq!(count, 0);
}
#[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
fn conn_attach_roundtrip(path: &std::path::Path) {
use crate::connection::SimpleConnection;
let conn = &mut connection();
conn.attach_database(path.to_str().unwrap(), "verbatim")
.unwrap();
conn.batch_execute("CREATE TABLE verbatim.attach_marker (id INTEGER PRIMARY KEY)")
.unwrap();
conn.detach_database("verbatim").unwrap();
}
#[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
#[diesel_test_helper::test]
fn attach_database_interprets_file_uri_query_parameters() {
let (_dir, path) = temp_db_path("uri_seed.db");
{
let mut seed = SqliteConnection::establish(path.to_str().unwrap()).unwrap();
crate::sql_query("CREATE TABLE t (id INTEGER PRIMARY KEY)")
.execute(&mut seed)
.unwrap();
crate::sql_query("INSERT INTO t (id) VALUES (1)")
.execute(&mut seed)
.unwrap();
}
let uri = format!("file:{}?mode=ro", path.display());
let conn = &mut connection();
conn.attach_database(&uri, "ro_schema").unwrap();
let id: i64 = sql::<crate::sql_types::BigInt>("SELECT id FROM ro_schema.t")
.get_result(conn)
.unwrap();
assert_eq!(id, 1);
assert!(
crate::sql_query("INSERT INTO ro_schema.t (id) VALUES (2)")
.execute(conn)
.is_err()
);
conn.detach_database("ro_schema").unwrap();
}
#[diesel_test_helper::test]
#[cfg(not(miri))] fn attach_and_detach_surface_errors_without_panicking() {
let conn = &mut connection();
conn.attach_database(":memory:", "dup").unwrap();
assert!(conn.attach_database(":memory:", "dup").is_err());
conn.detach_database("dup").unwrap();
assert!(conn.detach_database("never_attached").is_err());
}
#[diesel_test_helper::test]
fn attach_database_binds_schema_name_verbatim_without_identifier_quoting() {
use crate::connection::SimpleConnection;
let conn = &mut connection();
let schema = "weird 'schema";
conn.attach_database(":memory:", schema).unwrap();
conn.batch_execute(
r#"CREATE TABLE "weird 'schema".t (id INTEGER PRIMARY KEY);
INSERT INTO "weird 'schema".t (id) VALUES (7);"#,
)
.unwrap();
let id = sql::<Integer>(r#"SELECT id FROM "weird 'schema".t"#)
.get_result::<i32>(conn)
.unwrap();
assert_eq!(id, 7);
conn.detach_database(schema).unwrap();
}
#[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
#[diesel_test_helper::test]
fn attach_database_honors_create_and_write_hardening_knobs() {
use crate::connection::SimpleConnection;
let conn = &mut connection();
if conn.set_attach_create_enabled(false).is_err() {
return;
}
let (_dir_missing, missing) = temp_db_path("nocreate.db");
assert!(
conn.attach_database(missing.to_str().unwrap(), "missing")
.is_err()
);
assert!(!missing.exists());
conn.set_attach_write_enabled(false).unwrap();
let (_dir_existing, existing) = temp_db_path("readonly.db");
{
let mut seed = SqliteConnection::establish(existing.to_str().unwrap()).unwrap();
seed.batch_execute("CREATE TABLE readonly_marker (id INTEGER)")
.unwrap();
}
conn.attach_database(existing.to_str().unwrap(), "ro")
.unwrap();
assert!(
crate::insert_into(readonly_marker::table)
.values(readonly_marker::id.eq(1))
.execute(conn)
.is_err()
);
conn.detach_database("ro").unwrap();
}
#[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
#[diesel_test_helper::test]
fn attach_database_rejects_nul_in_path() {
let (_dir, path) = temp_db_path("nul-truncated.db");
let conn = &mut connection();
assert!(matches!(
conn.attach_database(&format!("{}\0ignored.db", path.display()), "nul"),
Err(Error::InvalidCString(_))
));
assert!(!path.exists());
}
#[diesel_test_helper::test]
#[cfg(not(miri))] fn attach_database_rejects_nul_in_schema_name() {
let conn = &mut connection();
assert!(matches!(
conn.attach_database(":memory:", "ab\0cd"),
Err(Error::InvalidCString(_))
));
assert!(conn.detach_database("ab").is_err());
}
#[diesel_test_helper::test]
fn detach_database_rejects_nul_in_schema_name() {
let conn = &mut connection();
assert!(matches!(
conn.detach_database("ab\0cd"),
Err(Error::InvalidCString(_))
));
}
}