Skip to main content

diesel/sqlite/connection/
attach.rs

1use super::SqliteConnection;
2use crate::query_builder::{AstPass, QueryFragment, QueryId};
3use crate::query_dsl::RunQueryDslSupport;
4use crate::result::QueryResult;
5use crate::sqlite::Sqlite;
6
7impl SqliteConnection {
8    /// Attach the database file at `path` under `schema_name`.
9    ///
10    /// Runs [`ATTACH DATABASE ? AS ?`](https://www.sqlite.org/lang_attach.html) with
11    /// both operands bound as parameters, so no SQL string escaping is needed.
12    /// Diesel always opens connections with `SQLITE_OPEN_URI`, so a path beginning
13    /// with `file:` is interpreted as a URI exactly as it is in
14    /// [`SqliteConnection::establish`](crate::connection::Connection::establish).
15    ///
16    /// A missing file is created as an empty database unless
17    /// [`set_attach_create_enabled(false)`][Self::set_attach_create_enabled] is set,
18    /// and [`set_attach_write_enabled(false)`][Self::set_attach_write_enabled] attaches
19    /// read-only. The attach count is bounded (10 by default). A transaction across the
20    /// main and an attached file is crash-atomic per file only under WAL.
21    ///
22    /// # Example
23    ///
24    /// ```rust
25    /// # include!("../../doctest_setup.rs");
26    /// #
27    /// # fn main() {
28    /// #     run_test().unwrap();
29    /// # }
30    /// #
31    /// # fn run_test() -> QueryResult<()> {
32    /// #     let conn = &mut SqliteConnection::establish(":memory:").unwrap();
33    /// conn.attach_database(":memory:", "aux")?;
34    /// conn.detach_database("aux")?;
35    /// #     Ok(())
36    /// # }
37    /// ```
38    pub fn attach_database(&mut self, path: &str, schema_name: &str) -> QueryResult<()> {
39        use crate::query_dsl::RunQueryDsl;
40        AttachDatabase { path, schema_name }
41            .execute(self)
42            .map(|_| ())
43    }
44
45    /// Detach the database previously attached under `schema_name`.
46    ///
47    /// Runs [`DETACH DATABASE ?`](https://www.sqlite.org/lang_detach.html) with the
48    /// schema name bound as a parameter. Detaching a schema still in use fails with an
49    /// ordinary error.
50    pub fn detach_database(&mut self, schema_name: &str) -> QueryResult<()> {
51        use crate::query_dsl::RunQueryDsl;
52        DetachDatabase { schema_name }.execute(self).map(|_| ())
53    }
54}
55
56#[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)]
57struct AttachDatabase<'a> {
58    path: &'a str,
59    schema_name: &'a str,
60}
61
62impl QueryFragment<Sqlite> for AttachDatabase<'_> {
63    fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Sqlite>) -> QueryResult<()> {
64        out.push_sql("ATTACH DATABASE ");
65        out.push_bind_param::<crate::sql_types::Text, _>(self.path)?;
66        out.push_sql(" AS ");
67        out.push_bind_param::<crate::sql_types::Text, _>(self.schema_name)?;
68        Ok(())
69    }
70}
71
72impl RunQueryDslSupport for AttachDatabase<'_> {}
73
74#[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)]
75struct DetachDatabase<'a> {
76    schema_name: &'a str,
77}
78
79impl QueryFragment<Sqlite> for DetachDatabase<'_> {
80    fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Sqlite>) -> QueryResult<()> {
81        out.push_sql("DETACH DATABASE ");
82        out.push_bind_param::<crate::sql_types::Text, _>(self.schema_name)?;
83        Ok(())
84    }
85}
86
87impl RunQueryDslSupport for DetachDatabase<'_> {}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use crate::dsl::sql;
93    use crate::prelude::*;
94    use crate::sql_types::Integer;
95
96    fn connection() -> SqliteConnection {
97        SqliteConnection::establish(":memory:").unwrap()
98    }
99
100    // These ATTACH tests need a real filesystem (temp files), which is not
101    // available on the wasm target, where SQLite is in-memory only.
102    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
103    fn temp_db_path(name: &str) -> (tempfile::TempDir, std::path::PathBuf) {
104        let dir = tempfile::tempdir().unwrap();
105        let path = dir.path().join(name);
106        (dir, path)
107    }
108
109    // Tables used by the ATTACH round-trip tests below. Their CREATE statements are
110    // DDL (raw SQL), but rows and reads go through the typed query DSL.
111    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
112    table! {
113        attach_owners (id) {
114            id -> Integer,
115            name -> Text,
116        }
117    }
118    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
119    table! {
120        aux.attach_pets (id) {
121            id -> Integer,
122            owner_id -> Integer,
123            name -> Text,
124        }
125    }
126    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
127    allow_tables_to_appear_in_same_query!(attach_owners, attach_pets);
128    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
129    table! {
130        attach_marker (id) {
131            id -> Integer,
132        }
133    }
134    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
135    table! {
136        ro.readonly_marker (id) {
137            id -> Integer,
138        }
139    }
140
141    // no miri as this returns a string
142    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
143    #[diesel_test_helper::test]
144    fn attach_database_supports_cross_schema_join_then_detach() {
145        use crate::connection::SimpleConnection;
146        let conn = &mut connection();
147
148        conn.attach_database(":memory:", "aux").unwrap();
149
150        // Schema-qualified CREATE is DDL and stays raw SQL. The rows and the join
151        // below use the typed query DSL.
152        conn.batch_execute(
153            "CREATE TABLE attach_owners (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
154             CREATE TABLE aux.attach_pets (id INTEGER PRIMARY KEY, owner_id INTEGER, name TEXT NOT NULL);",
155        )
156        .unwrap();
157
158        crate::insert_into(attach_owners::table)
159            .values(&[
160                (attach_owners::id.eq(1), attach_owners::name.eq("Sean")),
161                (attach_owners::id.eq(2), attach_owners::name.eq("Tess")),
162            ])
163            .execute(conn)
164            .unwrap();
165        crate::insert_into(attach_pets::table)
166            .values((
167                attach_pets::id.eq(1),
168                attach_pets::owner_id.eq(1),
169                attach_pets::name.eq("Ferris"),
170            ))
171            .execute(conn)
172            .unwrap();
173
174        let pet_owner = attach_owners::table
175            .inner_join(attach_pets::table.on(attach_pets::owner_id.eq(attach_owners::id)))
176            .filter(attach_pets::name.eq("Ferris"))
177            .select(attach_owners::name)
178            .get_result::<String>(conn)
179            .unwrap();
180        assert_eq!(pet_owner, "Sean");
181
182        conn.detach_database("aux").unwrap();
183
184        // The attached schema is gone, so querying it now fails.
185        assert!(
186            attach_pets::table
187                .select(attach_pets::name)
188                .get_result::<String>(conn)
189                .is_err()
190        );
191    }
192
193    // no miri, as that requires a fs call
194    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
195    #[diesel_test_helper::test]
196    fn attach_database_binds_path_verbatim_without_quoting() {
197        // A single quote in the path would break a hand-assembled ATTACH statement.
198        // Bound parameters take the path verbatim.
199        let (_dir, path) = temp_db_path("o'brien.db");
200
201        conn_attach_roundtrip(&path);
202
203        // The table created through ATTACH persisted to the literal file, so a
204        // fresh connection to that exact path can read it.
205        let mut direct = SqliteConnection::establish(path.to_str().unwrap()).unwrap();
206        let count = attach_marker::table
207            .count()
208            .get_result::<i64>(&mut direct)
209            .unwrap();
210        assert_eq!(count, 0);
211    }
212
213    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
214    fn conn_attach_roundtrip(path: &std::path::Path) {
215        use crate::connection::SimpleConnection;
216        let conn = &mut connection();
217        conn.attach_database(path.to_str().unwrap(), "verbatim")
218            .unwrap();
219        conn.batch_execute("CREATE TABLE verbatim.attach_marker (id INTEGER PRIMARY KEY)")
220            .unwrap();
221        conn.detach_database("verbatim").unwrap();
222    }
223
224    // no miri as that requires a fs call
225    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
226    #[diesel_test_helper::test]
227    fn attach_database_interprets_file_uri_query_parameters() {
228        // Seed a database with a row to read through the attached schema.
229        let (_dir, path) = temp_db_path("uri_seed.db");
230        {
231            let mut seed = SqliteConnection::establish(path.to_str().unwrap()).unwrap();
232            crate::sql_query("CREATE TABLE t (id INTEGER PRIMARY KEY)")
233                .execute(&mut seed)
234                .unwrap();
235            crate::sql_query("INSERT INTO t (id) VALUES (1)")
236                .execute(&mut seed)
237                .unwrap();
238        }
239
240        // Attach with a `file:` URI and `mode=ro`. If SQLite treated the bound
241        // string as a literal filename it would fail to find the file; interpreting
242        // it as a URI opens the real file in read-only mode instead.
243        let uri = format!("file:{}?mode=ro", path.display());
244        let conn = &mut connection();
245        conn.attach_database(&uri, "ro_schema").unwrap();
246
247        // Read from the attached schema: proves the ATTACH opened a real file.
248        let id: i64 = sql::<crate::sql_types::BigInt>("SELECT id FROM ro_schema.t")
249            .get_result(conn)
250            .unwrap();
251        assert_eq!(id, 1);
252
253        // Write fails: the URI `mode=ro` parameter took effect.
254        assert!(
255            crate::sql_query("INSERT INTO ro_schema.t (id) VALUES (2)")
256                .execute(conn)
257                .is_err()
258        );
259
260        conn.detach_database("ro_schema").unwrap();
261    }
262
263    #[diesel_test_helper::test]
264    #[cfg(not(miri))] // ffi string access
265    fn attach_and_detach_surface_errors_without_panicking() {
266        let conn = &mut connection();
267
268        // A duplicate schema name on attach is an error, not a panic.
269        conn.attach_database(":memory:", "dup").unwrap();
270        assert!(conn.attach_database(":memory:", "dup").is_err());
271        conn.detach_database("dup").unwrap();
272
273        // Detaching an unknown schema is likewise an error. Detaching one still in
274        // use cannot occur through the safe API: an in-flight iterator holds
275        // `&mut conn`, so no detach can overlap it.
276        assert!(conn.detach_database("never_attached").is_err());
277    }
278
279    #[diesel_test_helper::test]
280    fn attach_database_binds_schema_name_verbatim_without_identifier_quoting() {
281        use crate::connection::SimpleConnection;
282        let conn = &mut connection();
283
284        // A space or quote in the schema name would need identifier quoting in a
285        // hand-assembled statement. Bound as a parameter it is taken verbatim. The
286        // reference below stays raw SQL: such a schema is not expressible via `table!`.
287        let schema = "weird 'schema";
288        conn.attach_database(":memory:", schema).unwrap();
289
290        conn.batch_execute(
291            r#"CREATE TABLE "weird 'schema".t (id INTEGER PRIMARY KEY);
292             INSERT INTO "weird 'schema".t (id) VALUES (7);"#,
293        )
294        .unwrap();
295        let id = sql::<Integer>(r#"SELECT id FROM "weird 'schema".t"#)
296            .get_result::<i32>(conn)
297            .unwrap();
298        assert_eq!(id, 7);
299
300        conn.detach_database(schema).unwrap();
301    }
302
303    // no miri as that requires a fs call
304    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
305    #[diesel_test_helper::test]
306    fn attach_database_honors_create_and_write_hardening_knobs() {
307        use crate::connection::SimpleConnection;
308        let conn = &mut connection();
309
310        // ATTACH_CREATE and ATTACH_WRITE need SQLite 3.49.0+, so skip on older libraries.
311        if conn.set_attach_create_enabled(false).is_err() {
312            return;
313        }
314
315        // Create disabled: attaching a nonexistent path fails and materializes nothing.
316        let (_dir_missing, missing) = temp_db_path("nocreate.db");
317        assert!(
318            conn.attach_database(missing.to_str().unwrap(), "missing")
319                .is_err()
320        );
321        assert!(!missing.exists());
322
323        // Write disabled: an existing database attaches read-only, so writes fail.
324        conn.set_attach_write_enabled(false).unwrap();
325        let (_dir_existing, existing) = temp_db_path("readonly.db");
326        {
327            let mut seed = SqliteConnection::establish(existing.to_str().unwrap()).unwrap();
328            seed.batch_execute("CREATE TABLE readonly_marker (id INTEGER)")
329                .unwrap();
330        }
331        conn.attach_database(existing.to_str().unwrap(), "ro")
332            .unwrap();
333        assert!(
334            crate::insert_into(readonly_marker::table)
335                .values(readonly_marker::id.eq(1))
336                .execute(conn)
337                .is_err()
338        );
339        conn.detach_database("ro").unwrap();
340    }
341}