Skip to main content

diesel/sqlite/connection/
db_config.rs

1#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
2extern crate libsqlite3_sys as ffi;
3
4#[cfg(all(target_family = "wasm", target_os = "unknown"))]
5use sqlite_wasm_rs as ffi;
6
7use super::SqliteConnection;
8use super::raw;
9use crate::result::QueryResult;
10
11impl SqliteConnection {
12    /// Enable or disable SQLite defensive mode.
13    ///
14    /// When enabled, defensive mode prevents direct writes to shadow tables
15    /// (FTS5, R-Tree, etc.), dangerous PRAGMAs like `writable_schema`,
16    /// `sqlite3_deserialize()` from opening unsafe database images, and other
17    /// potentially dangerous operations. Enable it for any connection that may
18    /// process untrusted data. It is the single most important hardening flag.
19    ///
20    /// Requires SQLite 3.26.0 or later, otherwise returns an error.
21    ///
22    /// # Security Hardening Recipe
23    ///
24    /// ```rust
25    /// # include!("../../doctest_setup.rs");
26    /// # fn main() {
27    /// #     let mut conn = SqliteConnection::establish(":memory:").unwrap();
28    /// conn.set_defensive(true).unwrap();
29    /// conn.set_trusted_schema(false).unwrap();
30    /// conn.set_recommended_security_limits();
31    /// # }
32    /// ```
33    ///
34    /// Extension loading is off by default. Enable it only when needed via
35    /// [`with_load_extension_enabled`][Self::with_load_extension_enabled]. See
36    /// [`set_recommended_security_limits`][Self::set_recommended_security_limits]
37    /// to harden the SQLite resource limits as well.
38    pub fn set_defensive(&mut self, enabled: bool) -> QueryResult<()> {
39        self.raw_connection
40            .set_db_config_bool(ffi::SQLITE_DBCONFIG_DEFENSIVE, enabled)
41    }
42
43    /// Check if defensive mode is enabled.
44    ///
45    /// See [`set_defensive`][Self::set_defensive] for details.
46    pub fn is_defensive(&self) -> QueryResult<bool> {
47        self.raw_connection
48            .get_db_config_bool(ffi::SQLITE_DBCONFIG_DEFENSIVE)
49    }
50
51    /// Enable or disable trusted schema mode.
52    ///
53    /// When disabled (untrusted), SQL functions called from schema objects
54    /// (views, triggers, CHECK constraints, DEFAULT expressions, generated
55    /// columns, expression indexes) are restricted to those marked
56    /// [`INNOCUOUS`][crate::sqlite::SqliteFunctionBehavior::INNOCUOUS]. Disable
57    /// it when opening database files from untrusted sources, and register your
58    /// custom functions with appropriate
59    /// [`SqliteFunctionBehavior`][crate::sqlite::SqliteFunctionBehavior] flags.
60    ///
61    /// Requires SQLite 3.31.0 or later, otherwise returns an error.
62    pub fn set_trusted_schema(&mut self, trusted: bool) -> QueryResult<()> {
63        self.raw_connection
64            .set_db_config_bool(ffi::SQLITE_DBCONFIG_TRUSTED_SCHEMA, trusted)
65    }
66
67    /// Check if trusted schema mode is enabled.
68    ///
69    /// See [`set_trusted_schema`][Self::set_trusted_schema] for details.
70    pub fn is_trusted_schema(&self) -> QueryResult<bool> {
71        self.raw_connection
72            .get_db_config_bool(ffi::SQLITE_DBCONFIG_TRUSTED_SCHEMA)
73    }
74
75    /// Runs the given closure with the `load_extension()` SQL function enabled,
76    /// disabling it again afterwards.
77    ///
78    /// This controls the [`load_extension()`](https://www.sqlite.org/lang_corefunc.html#load_extension)
79    /// **SQL function**, not the `sqlite3_load_extension()` C API (which Diesel
80    /// does not expose). Extension loading is off by default, and scoping it to a
81    /// closure keeps the window in which it is enabled as small as possible.
82    ///
83    /// Requires SQLite 3.13.0 or later, otherwise returns an error. Has no effect
84    /// if SQLite was compiled with `SQLITE_OMIT_LOAD_EXTENSION`.
85    ///
86    /// # Panics
87    ///
88    /// If `f` panics, extension loading is disabled again before the panic
89    /// resumes. no-std builds cannot catch the unwind, so there the flag is
90    /// restored only on a normal return.
91    ///
92    /// # Example
93    ///
94    /// ```rust
95    /// # include!("../../doctest_setup.rs");
96    /// # fn main() {
97    /// #     let mut conn = SqliteConnection::establish(":memory:").unwrap();
98    /// let result: QueryResult<()> = conn.with_load_extension_enabled(|_conn| Ok(()));
99    /// result.unwrap();
100    /// # }
101    /// ```
102    pub fn with_load_extension_enabled<R, E>(
103        &mut self,
104        f: impl FnOnce(&mut Self) -> Result<R, E>,
105    ) -> Result<R, E>
106    where
107        E: From<crate::result::Error>,
108    {
109        self.set_load_extension_enabled(true)?;
110
111        // On std builds, catch a panic from `f` so extension loading is restored
112        // before the panic is resumed. no-std cannot catch unwinding, so there
113        // the flag is restored only on a normal return.
114        #[cfg(feature = "std")]
115        {
116            match std::panic::catch_unwind(core::panic::AssertUnwindSafe(|| f(self))) {
117                Ok(r) => {
118                    self.set_load_extension_enabled(false)?;
119                    r
120                }
121                Err(panic) => {
122                    let _ = self.set_load_extension_enabled(false);
123                    std::panic::resume_unwind(panic);
124                }
125            }
126        }
127        #[cfg(not(feature = "std"))]
128        {
129            let r = f(self);
130            self.set_load_extension_enabled(false)?;
131            r
132        }
133    }
134
135    fn set_load_extension_enabled(&mut self, enabled: bool) -> QueryResult<()> {
136        self.raw_connection
137            .set_db_config_bool(ffi::SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, enabled)
138    }
139
140    #[cfg(test)]
141    fn is_load_extension_enabled(&self) -> QueryResult<bool> {
142        self.raw_connection
143            .get_db_config_bool(ffi::SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION)
144    }
145
146    /// Enable or disable the `fts3_tokenizer()` SQL function.
147    ///
148    /// The [`fts3_tokenizer()`](https://www.sqlite.org/fts3.html#f3tknzr) function
149    /// allows overloading the default FTS3/FTS4 tokenizer, which can be exploited
150    /// if an attacker can execute arbitrary SQL. Disable it unless you need custom
151    /// FTS3 tokenizers.
152    ///
153    /// Requires SQLite 3.12.0 or later, otherwise returns an error.
154    pub fn set_fts3_tokenizer_enabled(&mut self, enabled: bool) -> QueryResult<()> {
155        self.raw_connection
156            .set_db_config_bool(ffi::SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER, enabled)
157    }
158
159    /// Check if the `fts3_tokenizer()` SQL function is enabled.
160    ///
161    /// See [`set_fts3_tokenizer_enabled`][Self::set_fts3_tokenizer_enabled] for details.
162    pub fn is_fts3_tokenizer_enabled(&self) -> QueryResult<bool> {
163        self.raw_connection
164            .get_db_config_bool(ffi::SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER)
165    }
166
167    /// Enable or disable direct writes to `sqlite_master`.
168    ///
169    /// When enabled, allows direct modification of the `sqlite_master` table,
170    /// which can corrupt the database if misused. Keep it disabled unless you
171    /// need to repair or modify the schema directly. Defensive mode
172    /// ([`set_defensive`][Self::set_defensive]) also prevents this.
173    ///
174    /// Requires SQLite 3.28.0 or later, otherwise returns an error.
175    pub fn set_writable_schema(&mut self, enabled: bool) -> QueryResult<()> {
176        self.raw_connection
177            .set_db_config_bool(ffi::SQLITE_DBCONFIG_WRITABLE_SCHEMA, enabled)
178    }
179
180    /// Check if direct writes to `sqlite_master` are enabled.
181    ///
182    /// See [`set_writable_schema`][Self::set_writable_schema] for details.
183    pub fn is_writable_schema(&self) -> QueryResult<bool> {
184        self.raw_connection
185            .get_db_config_bool(ffi::SQLITE_DBCONFIG_WRITABLE_SCHEMA)
186    }
187
188    /// Enable or disable ATTACH from creating new database files.
189    ///
190    /// When disabled, [`ATTACH`](https://www.sqlite.org/lang_attach.html) can only
191    /// open existing database files, not create new ones. Disable it where
192    /// database file creation should be restricted.
193    ///
194    /// Requires SQLite 3.49.0 or later, otherwise returns an error.
195    pub fn set_attach_create_enabled(&mut self, enabled: bool) -> QueryResult<()> {
196        self.raw_connection
197            .set_db_config_bool(raw::SQLITE_DBCONFIG_ENABLE_ATTACH_CREATE, enabled)
198    }
199
200    /// Check if ATTACH can create new database files.
201    ///
202    /// See [`set_attach_create_enabled`][Self::set_attach_create_enabled] for details.
203    pub fn is_attach_create_enabled(&self) -> QueryResult<bool> {
204        self.raw_connection
205            .get_db_config_bool(raw::SQLITE_DBCONFIG_ENABLE_ATTACH_CREATE)
206    }
207
208    /// Enable or disable ATTACH from opening databases in write mode.
209    ///
210    /// When disabled, all attached databases are opened as read-only. Disable it
211    /// to restrict write access to attached databases.
212    ///
213    /// Requires SQLite 3.49.0 or later, otherwise returns an error.
214    pub fn set_attach_write_enabled(&mut self, enabled: bool) -> QueryResult<()> {
215        self.raw_connection
216            .set_db_config_bool(raw::SQLITE_DBCONFIG_ENABLE_ATTACH_WRITE, enabled)
217    }
218
219    /// Check if ATTACH can open databases in write mode.
220    ///
221    /// See [`set_attach_write_enabled`][Self::set_attach_write_enabled] for details.
222    pub fn is_attach_write_enabled(&self) -> QueryResult<bool> {
223        self.raw_connection
224            .get_db_config_bool(raw::SQLITE_DBCONFIG_ENABLE_ATTACH_WRITE)
225    }
226
227    /// Enable or disable trigger execution.
228    ///
229    /// When disabled, triggers will not fire for any DML operations.
230    ///
231    /// Requires SQLite 3.8.7 or later, otherwise returns an error.
232    pub fn set_triggers_enabled(&mut self, enabled: bool) -> QueryResult<()> {
233        self.raw_connection
234            .set_db_config_bool(ffi::SQLITE_DBCONFIG_ENABLE_TRIGGER, enabled)
235    }
236
237    /// Check if trigger execution is enabled.
238    ///
239    /// See [`set_triggers_enabled`][Self::set_triggers_enabled] for details.
240    pub fn are_triggers_enabled(&self) -> QueryResult<bool> {
241        self.raw_connection
242            .get_db_config_bool(ffi::SQLITE_DBCONFIG_ENABLE_TRIGGER)
243    }
244
245    /// Enable or disable view expansion.
246    ///
247    /// When disabled, queries against views will fail.
248    ///
249    /// Requires SQLite 3.30.0 or later, otherwise returns an error.
250    pub fn set_views_enabled(&mut self, enabled: bool) -> QueryResult<()> {
251        self.raw_connection
252            .set_db_config_bool(ffi::SQLITE_DBCONFIG_ENABLE_VIEW, enabled)
253    }
254
255    /// Check if view expansion is enabled.
256    ///
257    /// See [`set_views_enabled`][Self::set_views_enabled] for details.
258    pub fn are_views_enabled(&self) -> QueryResult<bool> {
259        self.raw_connection
260            .get_db_config_bool(ffi::SQLITE_DBCONFIG_ENABLE_VIEW)
261    }
262
263    /// Enable or disable foreign key constraint enforcement.
264    ///
265    /// This is equivalent to `PRAGMA foreign_keys = ON/OFF`.
266    ///
267    /// Requires SQLite 3.8.7 or later, otherwise returns an error.
268    pub fn set_foreign_keys_enabled(&mut self, enabled: bool) -> QueryResult<()> {
269        self.raw_connection
270            .set_db_config_bool(ffi::SQLITE_DBCONFIG_ENABLE_FKEY, enabled)
271    }
272
273    /// Check if foreign key constraints are enabled.
274    ///
275    /// See [`set_foreign_keys_enabled`][Self::set_foreign_keys_enabled] for details.
276    pub fn are_foreign_keys_enabled(&self) -> QueryResult<bool> {
277        self.raw_connection
278            .get_db_config_bool(ffi::SQLITE_DBCONFIG_ENABLE_FKEY)
279    }
280
281    /// Enable or disable double-quoted strings in DML statements.
282    ///
283    /// When enabled, double-quoted strings are interpreted as string literals
284    /// rather than identifiers, a legacy behavior that can cause issues. Disable
285    /// it for stricter SQL compliance.
286    ///
287    /// Requires SQLite 3.29.0 or later, otherwise returns an error.
288    pub fn set_double_quoted_strings_dml(&mut self, enabled: bool) -> QueryResult<()> {
289        self.raw_connection
290            .set_db_config_bool(ffi::SQLITE_DBCONFIG_DQS_DML, enabled)
291    }
292
293    /// Check if double-quoted strings in DML are enabled.
294    ///
295    /// See [`set_double_quoted_strings_dml`][Self::set_double_quoted_strings_dml] for details.
296    pub fn are_double_quoted_strings_dml_enabled(&self) -> QueryResult<bool> {
297        self.raw_connection
298            .get_db_config_bool(ffi::SQLITE_DBCONFIG_DQS_DML)
299    }
300
301    /// Enable or disable double-quoted strings in DDL statements.
302    ///
303    /// When enabled, double-quoted strings are interpreted as string literals
304    /// rather than identifiers, a legacy behavior that can cause issues. Disable
305    /// it for stricter SQL compliance.
306    ///
307    /// Requires SQLite 3.29.0 or later, otherwise returns an error.
308    pub fn set_double_quoted_strings_ddl(&mut self, enabled: bool) -> QueryResult<()> {
309        self.raw_connection
310            .set_db_config_bool(ffi::SQLITE_DBCONFIG_DQS_DDL, enabled)
311    }
312
313    /// Check if double-quoted strings in DDL are enabled.
314    ///
315    /// See [`set_double_quoted_strings_ddl`][Self::set_double_quoted_strings_ddl] for details.
316    pub fn are_double_quoted_strings_ddl_enabled(&self) -> QueryResult<bool> {
317        self.raw_connection
318            .get_db_config_bool(ffi::SQLITE_DBCONFIG_DQS_DDL)
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325    use crate::dsl::sql;
326    use crate::prelude::*;
327    #[cfg(not(miri))]
328    use crate::sql_types::Text;
329
330    fn connection() -> SqliteConnection {
331        SqliteConnection::establish(":memory:").unwrap()
332    }
333
334    // ---- db_config tests ----
335
336    #[diesel_test_helper::test]
337    fn db_config_defensive_roundtrip() {
338        let conn = &mut connection();
339        conn.set_defensive(true).unwrap();
340        assert!(conn.is_defensive().unwrap());
341        conn.set_defensive(false).unwrap();
342        assert!(!conn.is_defensive().unwrap());
343    }
344
345    #[diesel_test_helper::test]
346    fn db_config_trusted_schema_roundtrip() {
347        let conn = &mut connection();
348        conn.set_trusted_schema(false).unwrap();
349        assert!(!conn.is_trusted_schema().unwrap());
350        conn.set_trusted_schema(true).unwrap();
351        assert!(conn.is_trusted_schema().unwrap());
352    }
353
354    #[diesel_test_helper::test]
355    fn db_config_with_load_extension_enabled_scopes_the_flag() {
356        let conn = &mut connection();
357        conn.with_load_extension_enabled(|conn| {
358            // Enabled for the duration of the closure.
359            assert!(conn.is_load_extension_enabled().unwrap());
360            QueryResult::Ok(())
361        })
362        .unwrap();
363        // Disabled again afterwards.
364        assert!(!conn.is_load_extension_enabled().unwrap());
365    }
366
367    #[cfg(all(
368        feature = "std",
369        not(all(target_family = "wasm", target_os = "unknown"))
370    ))]
371    #[diesel_test_helper::test]
372    fn with_load_extension_enabled_disables_after_panic() {
373        let conn = &mut connection();
374        let outcome = std::panic::catch_unwind(core::panic::AssertUnwindSafe(|| {
375            conn.with_load_extension_enabled(|_conn| -> QueryResult<()> {
376                panic!("boom inside closure");
377            })
378        }));
379        assert!(outcome.is_err(), "panic should propagate");
380        assert!(
381            !conn.is_load_extension_enabled().unwrap(),
382            "extension loading must be disabled again after a panic"
383        );
384    }
385
386    #[diesel_test_helper::test]
387    fn db_config_triggers_roundtrip() {
388        let conn = &mut connection();
389        conn.set_triggers_enabled(false).unwrap();
390        assert!(!conn.are_triggers_enabled().unwrap());
391        conn.set_triggers_enabled(true).unwrap();
392        assert!(conn.are_triggers_enabled().unwrap());
393    }
394
395    #[diesel_test_helper::test]
396    fn db_config_views_roundtrip() {
397        let conn = &mut connection();
398        conn.set_views_enabled(false).unwrap();
399        assert!(!conn.are_views_enabled().unwrap());
400        conn.set_views_enabled(true).unwrap();
401        assert!(conn.are_views_enabled().unwrap());
402    }
403
404    #[diesel_test_helper::test]
405    fn db_config_foreign_keys_roundtrip() {
406        let conn = &mut connection();
407        conn.set_foreign_keys_enabled(true).unwrap();
408        assert!(conn.are_foreign_keys_enabled().unwrap());
409        conn.set_foreign_keys_enabled(false).unwrap();
410        assert!(!conn.are_foreign_keys_enabled().unwrap());
411    }
412
413    #[diesel_test_helper::test]
414    fn db_config_dqs_dml_roundtrip() {
415        let conn = &mut connection();
416        conn.set_double_quoted_strings_dml(false).unwrap();
417        assert!(!conn.are_double_quoted_strings_dml_enabled().unwrap());
418        conn.set_double_quoted_strings_dml(true).unwrap();
419        assert!(conn.are_double_quoted_strings_dml_enabled().unwrap());
420    }
421
422    #[diesel_test_helper::test]
423    fn db_config_dqs_ddl_roundtrip() {
424        let conn = &mut connection();
425        conn.set_double_quoted_strings_ddl(false).unwrap();
426        assert!(!conn.are_double_quoted_strings_ddl_enabled().unwrap());
427        conn.set_double_quoted_strings_ddl(true).unwrap();
428        assert!(conn.are_double_quoted_strings_ddl_enabled().unwrap());
429    }
430
431    #[diesel_test_helper::test]
432    fn db_config_fts3_tokenizer_roundtrip() {
433        let conn = &mut connection();
434        conn.set_fts3_tokenizer_enabled(false).unwrap();
435        assert!(!conn.is_fts3_tokenizer_enabled().unwrap());
436        conn.set_fts3_tokenizer_enabled(true).unwrap();
437        assert!(conn.is_fts3_tokenizer_enabled().unwrap());
438    }
439
440    #[diesel_test_helper::test]
441    fn db_config_writable_schema_roundtrip() {
442        let conn = &mut connection();
443        conn.set_writable_schema(false).unwrap();
444        assert!(!conn.is_writable_schema().unwrap());
445        conn.set_writable_schema(true).unwrap();
446        assert!(conn.is_writable_schema().unwrap());
447    }
448
449    #[diesel_test_helper::test]
450    #[expect(unsafe_code, reason = "SQLite version lookup requires FFI")]
451    fn db_config_attach_create_roundtrip() {
452        // SAFETY: sqlite3_libversion_number has no caller preconditions.
453        if unsafe { ffi::sqlite3_libversion_number() } < 3_049_000 {
454            return;
455        }
456        let conn = &mut connection();
457        conn.set_attach_create_enabled(false).unwrap();
458        assert!(!conn.is_attach_create_enabled().unwrap());
459        conn.set_attach_create_enabled(true).unwrap();
460        assert!(conn.is_attach_create_enabled().unwrap());
461    }
462
463    #[diesel_test_helper::test]
464    #[expect(unsafe_code, reason = "SQLite version lookup requires FFI")]
465    fn db_config_attach_write_roundtrip() {
466        // SAFETY: sqlite3_libversion_number has no caller preconditions.
467        if unsafe { ffi::sqlite3_libversion_number() } < 3_049_000 {
468            return;
469        }
470        let conn = &mut connection();
471        conn.set_attach_write_enabled(false).unwrap();
472        assert!(!conn.is_attach_write_enabled().unwrap());
473        conn.set_attach_write_enabled(true).unwrap();
474        assert!(conn.is_attach_write_enabled().unwrap());
475    }
476
477    // ---- behavioral db_config tests ----
478
479    #[diesel_test_helper::test]
480    fn defensive_mode_blocks_writable_schema() {
481        let conn = &mut connection();
482        conn.set_defensive(true).unwrap();
483        // In defensive mode, writable_schema should remain off even if we try to set it
484        let _ = crate::sql_query("PRAGMA writable_schema = ON").execute(conn);
485        assert!(!conn.is_writable_schema().unwrap());
486    }
487
488    #[diesel_test_helper::test]
489    fn foreign_keys_enabled_enforces_constraints() {
490        let conn = &mut connection();
491        conn.set_foreign_keys_enabled(true).unwrap();
492
493        crate::sql_query("CREATE TABLE parent (id INTEGER PRIMARY KEY)")
494            .execute(conn)
495            .unwrap();
496        crate::sql_query(
497            "CREATE TABLE child (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent(id))",
498        )
499        .execute(conn)
500        .unwrap();
501
502        if cfg!(not(miri)) {
503            // fii string access
504            // Insert a child row with no matching parent — should fail with FK enabled
505            let result =
506                crate::sql_query("INSERT INTO child (id, parent_id) VALUES (1, 999)").execute(conn);
507            assert!(result.is_err());
508        }
509    }
510
511    #[diesel_test_helper::test]
512    fn views_disabled_blocks_view_queries() {
513        let conn = &mut connection();
514        crate::sql_query("CREATE TABLE base (id INTEGER PRIMARY KEY)")
515            .execute(conn)
516            .unwrap();
517        crate::sql_query("INSERT INTO base (id) VALUES (1)")
518            .execute(conn)
519            .unwrap();
520        crate::sql_query("CREATE VIEW base_view AS SELECT id FROM base")
521            .execute(conn)
522            .unwrap();
523
524        // Enabled (default): the view can be queried.
525        conn.set_views_enabled(true).unwrap();
526        assert!(
527            crate::sql_query("SELECT id FROM base_view")
528                .execute(conn)
529                .is_ok()
530        );
531
532        if cfg!(not(miri)) {
533            // ffi string access
534            // Disabled: queries that reference the view fail.
535            conn.set_views_enabled(false).unwrap();
536            assert!(
537                crate::sql_query("SELECT id FROM base_view")
538                    .execute(conn)
539                    .is_err()
540            );
541        }
542    }
543
544    #[diesel_test_helper::test]
545    fn triggers_disabled_prevents_firing() {
546        let conn = &mut connection();
547        crate::sql_query("CREATE TABLE source (id INTEGER PRIMARY KEY)")
548            .execute(conn)
549            .unwrap();
550        crate::sql_query("CREATE TABLE trigger_log (n INTEGER)")
551            .execute(conn)
552            .unwrap();
553        crate::sql_query("CREATE TRIGGER log_insert AFTER INSERT ON source BEGIN INSERT INTO trigger_log (n) VALUES (1); END")
554            .execute(conn)
555            .unwrap();
556
557        // Disabled: inserting into `source` must not fire the trigger.
558        conn.set_triggers_enabled(false).unwrap();
559        crate::sql_query("INSERT INTO source (id) VALUES (1)")
560            .execute(conn)
561            .unwrap();
562        let count: i64 = sql::<crate::sql_types::BigInt>("SELECT COUNT(*) FROM trigger_log")
563            .get_result(conn)
564            .unwrap();
565        assert_eq!(0, count, "trigger should not fire while disabled");
566
567        // Enabled: the trigger fires and writes one row.
568        conn.set_triggers_enabled(true).unwrap();
569        crate::sql_query("INSERT INTO source (id) VALUES (2)")
570            .execute(conn)
571            .unwrap();
572        let count: i64 = sql::<crate::sql_types::BigInt>("SELECT COUNT(*) FROM trigger_log")
573            .get_result(conn)
574            .unwrap();
575        assert_eq!(1, count, "trigger should fire while enabled");
576    }
577
578    #[cfg(not(miri))] // ffi string access
579    #[diesel_test_helper::test]
580    fn dqs_dml_controls_double_quoted_string_literals() {
581        let conn = &mut connection();
582
583        // Disabled: a double-quoted token in DML is parsed as an identifier, so a
584        // bare `"text"` that is not a column errors.
585        conn.set_double_quoted_strings_dml(false).unwrap();
586
587        let disabled = sql::<Text>(r#"SELECT "bare_token""#).get_result::<String>(conn);
588        assert!(disabled.is_err());
589
590        // Enabled: the same token is accepted as a string literal.
591        conn.set_double_quoted_strings_dml(true).unwrap();
592        let enabled = sql::<Text>(r#"SELECT "bare_token""#).get_result::<String>(conn);
593        assert_eq!(Ok("bare_token".to_owned()), enabled);
594    }
595
596    #[diesel_test_helper::test]
597    fn dqs_ddl_controls_double_quoted_string_literals() {
598        let conn = &mut connection();
599
600        // Disabled: a double-quoted token in a CHECK constraint is parsed as an
601        // identifier. As there is no such column, creating the table errors.
602        conn.set_double_quoted_strings_ddl(false).unwrap();
603        // accesses an ffi allocated string
604        if cfg!(not(miri)) {
605            let disabled = crate::sql_query(
606                r#"CREATE TABLE dqs_off (name TEXT, CHECK (name <> "not_a_column"))"#,
607            )
608            .execute(conn);
609            assert!(disabled.is_err());
610        }
611
612        // Enabled: the same token is accepted as a string literal, so the CHECK
613        // constraint (and the table) are created successfully.
614        conn.set_double_quoted_strings_ddl(true).unwrap();
615        let enabled =
616            crate::sql_query(r#"CREATE TABLE dqs_on (name TEXT, CHECK (name <> "not_a_column"))"#)
617                .execute(conn);
618        assert!(enabled.is_ok());
619    }
620
621    #[diesel_test_helper::test]
622    fn writable_schema_controls_direct_sqlite_master_writes() {
623        let conn = &mut connection();
624        crate::sql_query("CREATE TABLE protected (id INTEGER PRIMARY KEY)")
625            .execute(conn)
626            .unwrap();
627
628        let update =
629            "UPDATE sqlite_master SET sql = sql WHERE type = 'table' AND name = 'protected'";
630
631        if cfg!(not(miri)) {
632            // ffi string access
633            // Disabled (default): a direct write to sqlite_master is rejected.
634            conn.set_writable_schema(false).unwrap();
635            assert!(crate::sql_query(update).execute(conn).is_err());
636        }
637
638        // Enabled: the same write is permitted.
639        conn.set_writable_schema(true).unwrap();
640        assert!(crate::sql_query(update).execute(conn).is_ok());
641    }
642
643    #[cfg(not(miri))] // potential ffi string access
644    #[diesel_test_helper::test]
645    fn fts3_tokenizer_disabled_blocks_the_function() {
646        let conn = &mut connection();
647
648        // Enable first to detect whether FTS3 is compiled into this SQLite build.
649        conn.set_fts3_tokenizer_enabled(true).unwrap();
650        let enabled = sql::<crate::sql_types::Binary>("SELECT fts3_tokenizer('simple')")
651            .get_result::<Vec<u8>>(conn);
652        if enabled.is_err() {
653            // FTS3 is not available in this build, so there is nothing to assert.
654            return;
655        }
656
657        // Disabled: the `fts3_tokenizer()` SQL function is no longer callable.
658        conn.set_fts3_tokenizer_enabled(false).unwrap();
659        let disabled = sql::<crate::sql_types::Binary>("SELECT fts3_tokenizer('simple')")
660            .get_result::<Vec<u8>>(conn);
661        assert!(disabled.is_err());
662    }
663
664    // These ATTACH tests need a real filesystem (temp files), which is not
665    // available on the wasm target, where SQLite is in-memory only.
666    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
667    fn temp_db_path(name: &str) -> (tempfile::TempDir, std::path::PathBuf) {
668        let dir = tempfile::tempdir().unwrap();
669        let path = dir.path().join(name);
670        (dir, path)
671    }
672
673    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
674    #[diesel_test_helper::test]
675    fn attach_create_disabled_blocks_new_database_files() {
676        let conn = &mut connection();
677
678        // The ATTACH_CREATE option was added in SQLite 3.49.0; skip on older
679        // libraries (e.g. the system SQLite on the Ubuntu 24.04 CI runners).
680        if conn.set_attach_create_enabled(false).is_err() {
681            return;
682        }
683
684        let (_dir, path) = temp_db_path("create.db");
685
686        // Disabled: attaching a path that does not exist yet must fail.
687        assert!(
688            conn.attach_database(path.to_str().unwrap(), "aux_create")
689                .is_err()
690        );
691
692        // Enabled: the same ATTACH now creates and opens the file.
693        conn.set_attach_create_enabled(true).unwrap();
694        conn.attach_database(path.to_str().unwrap(), "aux_create")
695            .unwrap();
696        conn.detach_database("aux_create").unwrap();
697    }
698
699    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
700    #[diesel_test_helper::test]
701    fn attach_write_disabled_opens_attached_databases_read_only() {
702        let conn = &mut connection();
703
704        // The ATTACH_WRITE option was added in SQLite 3.49.0; skip on older
705        // libraries (e.g. the system SQLite on the Ubuntu 24.04 CI runners).
706        // This guard also leaves ATTACH_WRITE disabled for the first check below.
707        if conn.set_attach_write_enabled(false).is_err() {
708            return;
709        }
710
711        // Seed an existing on-disk database with a table to write into.
712        let (_dir, path) = temp_db_path("write.db");
713        {
714            let mut seed = SqliteConnection::establish(path.to_str().unwrap()).unwrap();
715            crate::sql_query("CREATE TABLE t (id INTEGER)")
716                .execute(&mut seed)
717                .unwrap();
718        }
719
720        // Disabled: the attached database is opened read-only, so writes fail.
721        conn.attach_database(path.to_str().unwrap(), "aux_write")
722            .unwrap();
723        assert!(
724            crate::sql_query("INSERT INTO aux_write.t (id) VALUES (1)")
725                .execute(conn)
726                .is_err()
727        );
728        conn.detach_database("aux_write").unwrap();
729
730        // Enabled: the attached database is writable again.
731        conn.set_attach_write_enabled(true).unwrap();
732        conn.attach_database(path.to_str().unwrap(), "aux_write")
733            .unwrap();
734        crate::sql_query("INSERT INTO aux_write.t (id) VALUES (1)")
735            .execute(conn)
736            .unwrap();
737        conn.detach_database("aux_write").unwrap();
738    }
739}