diesel/sqlite/connection/
attach.rs1use super::SqliteConnection;
2use crate::query_builder::{AstPass, QueryFragment, QueryId};
3use crate::query_dsl::RunQueryDslSupport;
4use crate::result::QueryResult;
5use crate::sqlite::Sqlite;
6use alloc::ffi::CString;
7
8impl SqliteConnection {
9 pub fn attach_database(&mut self, path: &str, schema_name: &str) -> QueryResult<()> {
40 use crate::query_dsl::RunQueryDsl;
41 CString::new(path)?;
42 CString::new(schema_name)?;
43 AttachDatabase { path, schema_name }
44 .execute(self)
45 .map(|_| ())
46 }
47
48 pub fn detach_database(&mut self, schema_name: &str) -> QueryResult<()> {
54 use crate::query_dsl::RunQueryDsl;
55 CString::new(schema_name)?;
56 DetachDatabase { schema_name }.execute(self).map(|_| ())
57 }
58}
59
60#[derive(const _: () =
{
use diesel;
#[allow(non_camel_case_types)]
impl<'a> diesel::query_builder::QueryId for AttachDatabase<'a> {
type QueryId = AttachDatabase<'static>;
const HAS_STATIC_QUERY_ID: bool = true;
const IS_WINDOW_FUNCTION: bool = false;
}
};QueryId)]
61struct AttachDatabase<'a> {
62 path: &'a str,
63 schema_name: &'a str,
64}
65
66impl QueryFragment<Sqlite> for AttachDatabase<'_> {
67 fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Sqlite>) -> QueryResult<()> {
68 out.push_sql("ATTACH DATABASE ");
69 out.push_bind_param::<crate::sql_types::Text, _>(self.path)?;
70 out.push_sql(" AS ");
71 out.push_bind_param::<crate::sql_types::Text, _>(self.schema_name)?;
72 Ok(())
73 }
74}
75
76impl RunQueryDslSupport for AttachDatabase<'_> {}
77
78#[derive(const _: () =
{
use diesel;
#[allow(non_camel_case_types)]
impl<'a> diesel::query_builder::QueryId for DetachDatabase<'a> {
type QueryId = DetachDatabase<'static>;
const HAS_STATIC_QUERY_ID: bool = true;
const IS_WINDOW_FUNCTION: bool = false;
}
};QueryId)]
79struct DetachDatabase<'a> {
80 schema_name: &'a str,
81}
82
83impl QueryFragment<Sqlite> for DetachDatabase<'_> {
84 fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Sqlite>) -> QueryResult<()> {
85 out.push_sql("DETACH DATABASE ");
86 out.push_bind_param::<crate::sql_types::Text, _>(self.schema_name)?;
87 Ok(())
88 }
89}
90
91impl RunQueryDslSupport for DetachDatabase<'_> {}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use crate::dsl::sql;
97 use crate::prelude::*;
98 use crate::result::Error;
99 use crate::sql_types::Integer;
100
101 fn connection() -> SqliteConnection {
102 SqliteConnection::establish(":memory:").unwrap()
103 }
104
105 #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
108 fn temp_db_path(name: &str) -> (tempfile::TempDir, std::path::PathBuf) {
109 let dir = tempfile::tempdir().unwrap();
110 let path = dir.path().join(name);
111 (dir, path)
112 }
113
114 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
117 table! {
118 attach_owners (id) {
119 id -> Integer,
120 name -> Text,
121 }
122 }
123 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
124 table! {
125 aux.attach_pets (id) {
126 id -> Integer,
127 owner_id -> Integer,
128 name -> Text,
129 }
130 }
131 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
132 allow_tables_to_appear_in_same_query!(attach_owners, attach_pets);
133 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
134 table! {
135 attach_marker (id) {
136 id -> Integer,
137 }
138 }
139 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
140 table! {
141 ro.readonly_marker (id) {
142 id -> Integer,
143 }
144 }
145
146 #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
148 #[diesel_test_helper::test]
149 fn attach_database_supports_cross_schema_join_then_detach() {
150 use crate::connection::SimpleConnection;
151 let conn = &mut connection();
152
153 conn.attach_database(":memory:", "aux").unwrap();
154
155 conn.batch_execute(
158 "CREATE TABLE attach_owners (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
159 CREATE TABLE aux.attach_pets (id INTEGER PRIMARY KEY, owner_id INTEGER, name TEXT NOT NULL);",
160 )
161 .unwrap();
162
163 crate::insert_into(attach_owners::table)
164 .values(&[
165 (attach_owners::id.eq(1), attach_owners::name.eq("Sean")),
166 (attach_owners::id.eq(2), attach_owners::name.eq("Tess")),
167 ])
168 .execute(conn)
169 .unwrap();
170 crate::insert_into(attach_pets::table)
171 .values((
172 attach_pets::id.eq(1),
173 attach_pets::owner_id.eq(1),
174 attach_pets::name.eq("Ferris"),
175 ))
176 .execute(conn)
177 .unwrap();
178
179 let pet_owner = attach_owners::table
180 .inner_join(attach_pets::table.on(attach_pets::owner_id.eq(attach_owners::id)))
181 .filter(attach_pets::name.eq("Ferris"))
182 .select(attach_owners::name)
183 .get_result::<String>(conn)
184 .unwrap();
185 assert_eq!(pet_owner, "Sean");
186
187 conn.detach_database("aux").unwrap();
188
189 assert!(
191 attach_pets::table
192 .select(attach_pets::name)
193 .get_result::<String>(conn)
194 .is_err()
195 );
196 }
197
198 #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
200 #[diesel_test_helper::test]
201 fn attach_database_binds_path_verbatim_without_quoting() {
202 let (_dir, path) = temp_db_path("o'brien.db");
205
206 conn_attach_roundtrip(&path);
207
208 let mut direct = SqliteConnection::establish(path.to_str().unwrap()).unwrap();
211 let count = attach_marker::table
212 .count()
213 .get_result::<i64>(&mut direct)
214 .unwrap();
215 assert_eq!(count, 0);
216 }
217
218 #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
219 fn conn_attach_roundtrip(path: &std::path::Path) {
220 use crate::connection::SimpleConnection;
221 let conn = &mut connection();
222 conn.attach_database(path.to_str().unwrap(), "verbatim")
223 .unwrap();
224 conn.batch_execute("CREATE TABLE verbatim.attach_marker (id INTEGER PRIMARY KEY)")
225 .unwrap();
226 conn.detach_database("verbatim").unwrap();
227 }
228
229 #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
231 #[diesel_test_helper::test]
232 fn attach_database_interprets_file_uri_query_parameters() {
233 let (_dir, path) = temp_db_path("uri_seed.db");
235 {
236 let mut seed = SqliteConnection::establish(path.to_str().unwrap()).unwrap();
237 crate::sql_query("CREATE TABLE t (id INTEGER PRIMARY KEY)")
238 .execute(&mut seed)
239 .unwrap();
240 crate::sql_query("INSERT INTO t (id) VALUES (1)")
241 .execute(&mut seed)
242 .unwrap();
243 }
244
245 let uri = format!("file:{}?mode=ro", path.display());
249 let conn = &mut connection();
250 conn.attach_database(&uri, "ro_schema").unwrap();
251
252 let id: i64 = sql::<crate::sql_types::BigInt>("SELECT id FROM ro_schema.t")
254 .get_result(conn)
255 .unwrap();
256 assert_eq!(id, 1);
257
258 assert!(
260 crate::sql_query("INSERT INTO ro_schema.t (id) VALUES (2)")
261 .execute(conn)
262 .is_err()
263 );
264
265 conn.detach_database("ro_schema").unwrap();
266 }
267
268 #[diesel_test_helper::test]
269 #[cfg(not(miri))] fn attach_and_detach_surface_errors_without_panicking() {
271 let conn = &mut connection();
272
273 conn.attach_database(":memory:", "dup").unwrap();
275 assert!(conn.attach_database(":memory:", "dup").is_err());
276 conn.detach_database("dup").unwrap();
277
278 assert!(conn.detach_database("never_attached").is_err());
282 }
283
284 #[diesel_test_helper::test]
285 fn attach_database_binds_schema_name_verbatim_without_identifier_quoting() {
286 use crate::connection::SimpleConnection;
287 let conn = &mut connection();
288
289 let schema = "weird 'schema";
293 conn.attach_database(":memory:", schema).unwrap();
294
295 conn.batch_execute(
296 r#"CREATE TABLE "weird 'schema".t (id INTEGER PRIMARY KEY);
297 INSERT INTO "weird 'schema".t (id) VALUES (7);"#,
298 )
299 .unwrap();
300 let id = sql::<Integer>(r#"SELECT id FROM "weird 'schema".t"#)
301 .get_result::<i32>(conn)
302 .unwrap();
303 assert_eq!(id, 7);
304
305 conn.detach_database(schema).unwrap();
306 }
307
308 #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
310 #[diesel_test_helper::test]
311 fn attach_database_honors_create_and_write_hardening_knobs() {
312 use crate::connection::SimpleConnection;
313 let conn = &mut connection();
314
315 if conn.set_attach_create_enabled(false).is_err() {
317 return;
318 }
319
320 let (_dir_missing, missing) = temp_db_path("nocreate.db");
322 assert!(
323 conn.attach_database(missing.to_str().unwrap(), "missing")
324 .is_err()
325 );
326 assert!(!missing.exists());
327
328 conn.set_attach_write_enabled(false).unwrap();
330 let (_dir_existing, existing) = temp_db_path("readonly.db");
331 {
332 let mut seed = SqliteConnection::establish(existing.to_str().unwrap()).unwrap();
333 seed.batch_execute("CREATE TABLE readonly_marker (id INTEGER)")
334 .unwrap();
335 }
336 conn.attach_database(existing.to_str().unwrap(), "ro")
337 .unwrap();
338 assert!(
339 crate::insert_into(readonly_marker::table)
340 .values(readonly_marker::id.eq(1))
341 .execute(conn)
342 .is_err()
343 );
344 conn.detach_database("ro").unwrap();
345 }
346
347 #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
349 #[diesel_test_helper::test]
350 fn attach_database_rejects_nul_in_path() {
351 let (_dir, path) = temp_db_path("nul-truncated.db");
352
353 let conn = &mut connection();
354 assert!(matches!(
355 conn.attach_database(&format!("{}\0ignored.db", path.display()), "nul"),
356 Err(Error::InvalidCString(_))
357 ));
358 assert!(!path.exists());
359 }
360
361 #[diesel_test_helper::test]
362 #[cfg(not(miri))] fn attach_database_rejects_nul_in_schema_name() {
364 let conn = &mut connection();
365 assert!(matches!(
366 conn.attach_database(":memory:", "ab\0cd"),
367 Err(Error::InvalidCString(_))
368 ));
369 assert!(conn.detach_database("ab").is_err());
371 }
372
373 #[diesel_test_helper::test]
374 fn detach_database_rejects_nul_in_schema_name() {
375 let conn = &mut connection();
376 assert!(matches!(
377 conn.detach_database("ab\0cd"),
378 Err(Error::InvalidCString(_))
379 ));
380 }
381}