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 two-argument `fts3_tokenizer()` SQL function, which Diesel disables on every new connection.
147    ///
148    /// Requires SQLite 3.12.0 or later, otherwise returns an error.
149    ///
150    /// # Safety
151    ///
152    /// While it is enabled, SQL text can make SQLite call through any pointer, so no untrusted SQL may run on this connection.
153    #[allow(unsafe_code)]
154    pub unsafe 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::connection::DatabaseVersion;
326    use crate::dsl::sql;
327    use crate::prelude::*;
328    #[cfg(not(miri))]
329    use crate::sql_types::Text;
330
331    fn connection() -> SqliteConnection {
332        SqliteConnection::establish(":memory:").unwrap()
333    }
334
335    // ---- db_config tests ----
336
337    #[diesel_test_helper::test]
338    fn db_config_defensive_roundtrip() {
339        let conn = &mut connection();
340        conn.set_defensive(true).unwrap();
341        assert!(conn.is_defensive().unwrap());
342        conn.set_defensive(false).unwrap();
343        assert!(!conn.is_defensive().unwrap());
344    }
345
346    #[diesel_test_helper::test]
347    fn db_config_trusted_schema_roundtrip() {
348        let conn = &mut connection();
349        conn.set_trusted_schema(false).unwrap();
350        assert!(!conn.is_trusted_schema().unwrap());
351        conn.set_trusted_schema(true).unwrap();
352        assert!(conn.is_trusted_schema().unwrap());
353    }
354
355    #[diesel_test_helper::test]
356    fn db_config_with_load_extension_enabled_scopes_the_flag() {
357        let conn = &mut connection();
358        conn.with_load_extension_enabled(|conn| {
359            // Enabled for the duration of the closure.
360            assert!(conn.is_load_extension_enabled().unwrap());
361            QueryResult::Ok(())
362        })
363        .unwrap();
364        // Disabled again afterwards.
365        assert!(!conn.is_load_extension_enabled().unwrap());
366    }
367
368    #[cfg(all(
369        feature = "std",
370        not(all(target_family = "wasm", target_os = "unknown"))
371    ))]
372    #[diesel_test_helper::test]
373    fn with_load_extension_enabled_disables_after_panic() {
374        let conn = &mut connection();
375        let outcome = std::panic::catch_unwind(core::panic::AssertUnwindSafe(|| {
376            conn.with_load_extension_enabled(|_conn| -> QueryResult<()> {
377                panic!("boom inside closure");
378            })
379        }));
380        assert!(outcome.is_err(), "panic should propagate");
381        assert!(
382            !conn.is_load_extension_enabled().unwrap(),
383            "extension loading must be disabled again after a panic"
384        );
385    }
386
387    #[diesel_test_helper::test]
388    fn db_config_triggers_roundtrip() {
389        let conn = &mut connection();
390        conn.set_triggers_enabled(false).unwrap();
391        assert!(!conn.are_triggers_enabled().unwrap());
392        conn.set_triggers_enabled(true).unwrap();
393        assert!(conn.are_triggers_enabled().unwrap());
394    }
395
396    #[diesel_test_helper::test]
397    fn db_config_views_roundtrip() {
398        let conn = &mut connection();
399        conn.set_views_enabled(false).unwrap();
400        assert!(!conn.are_views_enabled().unwrap());
401        conn.set_views_enabled(true).unwrap();
402        assert!(conn.are_views_enabled().unwrap());
403    }
404
405    #[diesel_test_helper::test]
406    fn db_config_foreign_keys_roundtrip() {
407        let conn = &mut connection();
408        conn.set_foreign_keys_enabled(true).unwrap();
409        assert!(conn.are_foreign_keys_enabled().unwrap());
410        conn.set_foreign_keys_enabled(false).unwrap();
411        assert!(!conn.are_foreign_keys_enabled().unwrap());
412    }
413
414    #[diesel_test_helper::test]
415    fn db_config_dqs_dml_roundtrip() {
416        let conn = &mut connection();
417        conn.set_double_quoted_strings_dml(false).unwrap();
418        assert!(!conn.are_double_quoted_strings_dml_enabled().unwrap());
419        conn.set_double_quoted_strings_dml(true).unwrap();
420        assert!(conn.are_double_quoted_strings_dml_enabled().unwrap());
421    }
422
423    #[diesel_test_helper::test]
424    fn db_config_dqs_ddl_roundtrip() {
425        let conn = &mut connection();
426        conn.set_double_quoted_strings_ddl(false).unwrap();
427        assert!(!conn.are_double_quoted_strings_ddl_enabled().unwrap());
428        conn.set_double_quoted_strings_ddl(true).unwrap();
429        assert!(conn.are_double_quoted_strings_ddl_enabled().unwrap());
430    }
431
432    #[diesel_test_helper::test]
433    #[expect(unsafe_code, reason = "enabling the legacy tokenizer is unsafe")]
434    fn db_config_fts3_tokenizer_roundtrip() {
435        let conn = &mut connection();
436        // SAFETY: this connection only runs the test's own SQL.
437        unsafe { conn.set_fts3_tokenizer_enabled(false) }.unwrap();
438        assert!(!conn.is_fts3_tokenizer_enabled().unwrap());
439        // SAFETY: this connection only runs the test's own SQL.
440        unsafe { conn.set_fts3_tokenizer_enabled(true) }.unwrap();
441        assert!(conn.is_fts3_tokenizer_enabled().unwrap());
442    }
443
444    #[cfg(not(miri))] // ffi string access
445    #[diesel_test_helper::test]
446    fn new_connection_rejects_a_tokenizer_pointer_from_sql_text() {
447        use crate::connection::SimpleConnection;
448
449        let conn = &mut connection();
450        assert!(!conn.is_fts3_tokenizer_enabled().unwrap());
451        assert!(
452            conn.batch_execute(
453                "SELECT fts3_tokenizer('evil', x'4141414141414141'); \
454                 CREATE VIRTUAL TABLE evil_fts USING fts3(x, tokenize=evil);"
455            )
456            .is_err()
457        );
458    }
459
460    #[diesel_test_helper::test]
461    fn db_config_writable_schema_roundtrip() {
462        let conn = &mut connection();
463        conn.set_writable_schema(false).unwrap();
464        assert!(!conn.is_writable_schema().unwrap());
465        conn.set_writable_schema(true).unwrap();
466        assert!(conn.is_writable_schema().unwrap());
467    }
468
469    #[diesel_test_helper::test]
470    fn db_config_attach_create_roundtrip() {
471        let conn = &mut connection();
472        if conn.version() < DatabaseVersion::new(3, 49, 0) {
473            return;
474        }
475        conn.set_attach_create_enabled(false).unwrap();
476        assert!(!conn.is_attach_create_enabled().unwrap());
477        conn.set_attach_create_enabled(true).unwrap();
478        assert!(conn.is_attach_create_enabled().unwrap());
479    }
480
481    #[diesel_test_helper::test]
482    fn db_config_attach_write_roundtrip() {
483        let conn = &mut connection();
484        if conn.version() < DatabaseVersion::new(3, 49, 0) {
485            return;
486        }
487        conn.set_attach_write_enabled(false).unwrap();
488        assert!(!conn.is_attach_write_enabled().unwrap());
489        conn.set_attach_write_enabled(true).unwrap();
490        assert!(conn.is_attach_write_enabled().unwrap());
491    }
492
493    // ---- behavioral db_config tests ----
494
495    #[diesel_test_helper::test]
496    fn defensive_mode_blocks_writable_schema() {
497        let conn = &mut connection();
498        conn.set_defensive(true).unwrap();
499        // In defensive mode, writable_schema should remain off even if we try to set it
500        let _ = crate::sql_query("PRAGMA writable_schema = ON").execute(conn);
501        assert!(!conn.is_writable_schema().unwrap());
502    }
503
504    #[diesel_test_helper::test]
505    fn foreign_keys_enabled_enforces_constraints() {
506        let conn = &mut connection();
507        conn.set_foreign_keys_enabled(true).unwrap();
508
509        crate::sql_query("CREATE TABLE parent (id INTEGER PRIMARY KEY)")
510            .execute(conn)
511            .unwrap();
512        crate::sql_query(
513            "CREATE TABLE child (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent(id))",
514        )
515        .execute(conn)
516        .unwrap();
517
518        if cfg!(not(miri)) {
519            // fii string access
520            // Insert a child row with no matching parent — should fail with FK enabled
521            let result =
522                crate::sql_query("INSERT INTO child (id, parent_id) VALUES (1, 999)").execute(conn);
523            assert!(result.is_err());
524        }
525    }
526
527    #[diesel_test_helper::test]
528    fn views_disabled_blocks_view_queries() {
529        let conn = &mut connection();
530        crate::sql_query("CREATE TABLE base (id INTEGER PRIMARY KEY)")
531            .execute(conn)
532            .unwrap();
533        crate::sql_query("INSERT INTO base (id) VALUES (1)")
534            .execute(conn)
535            .unwrap();
536        crate::sql_query("CREATE VIEW base_view AS SELECT id FROM base")
537            .execute(conn)
538            .unwrap();
539
540        // Enabled (default): the view can be queried.
541        conn.set_views_enabled(true).unwrap();
542        assert!(
543            crate::sql_query("SELECT id FROM base_view")
544                .execute(conn)
545                .is_ok()
546        );
547
548        if cfg!(not(miri)) {
549            // ffi string access
550            // Disabled: queries that reference the view fail.
551            conn.set_views_enabled(false).unwrap();
552            assert!(
553                crate::sql_query("SELECT id FROM base_view")
554                    .execute(conn)
555                    .is_err()
556            );
557        }
558    }
559
560    #[diesel_test_helper::test]
561    fn triggers_disabled_prevents_firing() {
562        let conn = &mut connection();
563        crate::sql_query("CREATE TABLE source (id INTEGER PRIMARY KEY)")
564            .execute(conn)
565            .unwrap();
566        crate::sql_query("CREATE TABLE trigger_log (n INTEGER)")
567            .execute(conn)
568            .unwrap();
569        crate::sql_query("CREATE TRIGGER log_insert AFTER INSERT ON source BEGIN INSERT INTO trigger_log (n) VALUES (1); END")
570            .execute(conn)
571            .unwrap();
572
573        // Disabled: inserting into `source` must not fire the trigger.
574        conn.set_triggers_enabled(false).unwrap();
575        crate::sql_query("INSERT INTO source (id) VALUES (1)")
576            .execute(conn)
577            .unwrap();
578        let count: i64 = sql::<crate::sql_types::BigInt>("SELECT COUNT(*) FROM trigger_log")
579            .get_result(conn)
580            .unwrap();
581        assert_eq!(0, count, "trigger should not fire while disabled");
582
583        // Enabled: the trigger fires and writes one row.
584        conn.set_triggers_enabled(true).unwrap();
585        crate::sql_query("INSERT INTO source (id) VALUES (2)")
586            .execute(conn)
587            .unwrap();
588        let count: i64 = sql::<crate::sql_types::BigInt>("SELECT COUNT(*) FROM trigger_log")
589            .get_result(conn)
590            .unwrap();
591        assert_eq!(1, count, "trigger should fire while enabled");
592    }
593
594    #[cfg(not(miri))] // ffi string access
595    #[diesel_test_helper::test]
596    fn dqs_dml_controls_double_quoted_string_literals() {
597        let conn = &mut connection();
598
599        // Disabled: a double-quoted token in DML is parsed as an identifier, so a
600        // bare `"text"` that is not a column errors.
601        conn.set_double_quoted_strings_dml(false).unwrap();
602
603        let disabled = sql::<Text>(r#"SELECT "bare_token""#).get_result::<String>(conn);
604        assert!(disabled.is_err());
605
606        // Enabled: the same token is accepted as a string literal.
607        conn.set_double_quoted_strings_dml(true).unwrap();
608        let enabled = sql::<Text>(r#"SELECT "bare_token""#).get_result::<String>(conn);
609        assert_eq!(Ok("bare_token".to_owned()), enabled);
610    }
611
612    #[diesel_test_helper::test]
613    fn dqs_ddl_controls_double_quoted_string_literals() {
614        let conn = &mut connection();
615
616        // Disabled: a double-quoted token in a CHECK constraint is parsed as an
617        // identifier. As there is no such column, creating the table errors.
618        conn.set_double_quoted_strings_ddl(false).unwrap();
619        // accesses an ffi allocated string
620        if cfg!(not(miri)) {
621            let disabled = crate::sql_query(
622                r#"CREATE TABLE dqs_off (name TEXT, CHECK (name <> "not_a_column"))"#,
623            )
624            .execute(conn);
625            assert!(disabled.is_err());
626        }
627
628        // Enabled: the same token is accepted as a string literal, so the CHECK
629        // constraint (and the table) are created successfully.
630        conn.set_double_quoted_strings_ddl(true).unwrap();
631        let enabled =
632            crate::sql_query(r#"CREATE TABLE dqs_on (name TEXT, CHECK (name <> "not_a_column"))"#)
633                .execute(conn);
634        assert!(enabled.is_ok());
635    }
636
637    #[diesel_test_helper::test]
638    fn writable_schema_controls_direct_sqlite_master_writes() {
639        let conn = &mut connection();
640        crate::sql_query("CREATE TABLE protected (id INTEGER PRIMARY KEY)")
641            .execute(conn)
642            .unwrap();
643
644        let update =
645            "UPDATE sqlite_master SET sql = sql WHERE type = 'table' AND name = 'protected'";
646
647        if cfg!(not(miri)) {
648            // ffi string access
649            // Disabled (default): a direct write to sqlite_master is rejected.
650            conn.set_writable_schema(false).unwrap();
651            assert!(crate::sql_query(update).execute(conn).is_err());
652        }
653
654        // Enabled: the same write is permitted.
655        conn.set_writable_schema(true).unwrap();
656        assert!(crate::sql_query(update).execute(conn).is_ok());
657    }
658
659    #[cfg(not(miri))] // potential ffi string access
660    #[diesel_test_helper::test]
661    #[expect(unsafe_code, reason = "enabling the legacy tokenizer is unsafe")]
662    fn fts3_tokenizer_disabled_blocks_the_function() {
663        let conn = &mut connection();
664
665        // Enable first to detect whether FTS3 is compiled into this SQLite build.
666        // SAFETY: this connection only runs the test's own SQL.
667        unsafe { conn.set_fts3_tokenizer_enabled(true) }.unwrap();
668        let enabled = sql::<crate::sql_types::Binary>("SELECT fts3_tokenizer('simple')")
669            .get_result::<Vec<u8>>(conn);
670        if enabled.is_err() {
671            // FTS3 is not available in this build, so there is nothing to assert.
672            return;
673        }
674
675        // Disabled: the `fts3_tokenizer()` SQL function is no longer callable.
676        // SAFETY: disabling the legacy tokenizer cannot expose a pointer call.
677        unsafe { conn.set_fts3_tokenizer_enabled(false) }.unwrap();
678        let disabled = sql::<crate::sql_types::Binary>("SELECT fts3_tokenizer('simple')")
679            .get_result::<Vec<u8>>(conn);
680        assert!(disabled.is_err());
681    }
682
683    // These ATTACH tests need a real filesystem (temp files), which is not
684    // available on the wasm target, where SQLite is in-memory only.
685    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
686    fn temp_db_path(name: &str) -> (tempfile::TempDir, std::path::PathBuf) {
687        let dir = tempfile::tempdir().unwrap();
688        let path = dir.path().join(name);
689        (dir, path)
690    }
691
692    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
693    #[diesel_test_helper::test]
694    fn attach_create_disabled_blocks_new_database_files() {
695        let conn = &mut connection();
696
697        // The ATTACH_CREATE option was added in SQLite 3.49.0; skip on older
698        // libraries (e.g. the system SQLite on the Ubuntu 24.04 CI runners).
699        if conn.set_attach_create_enabled(false).is_err() {
700            return;
701        }
702
703        let (_dir, path) = temp_db_path("create.db");
704
705        // Disabled: attaching a path that does not exist yet must fail.
706        assert!(
707            conn.attach_database(path.to_str().unwrap(), "aux_create")
708                .is_err()
709        );
710
711        // Enabled: the same ATTACH now creates and opens the file.
712        conn.set_attach_create_enabled(true).unwrap();
713        conn.attach_database(path.to_str().unwrap(), "aux_create")
714            .unwrap();
715        conn.detach_database("aux_create").unwrap();
716    }
717
718    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
719    #[diesel_test_helper::test]
720    fn attach_write_disabled_opens_attached_databases_read_only() {
721        let conn = &mut connection();
722
723        // The ATTACH_WRITE option was added in SQLite 3.49.0; skip on older
724        // libraries (e.g. the system SQLite on the Ubuntu 24.04 CI runners).
725        // This guard also leaves ATTACH_WRITE disabled for the first check below.
726        if conn.set_attach_write_enabled(false).is_err() {
727            return;
728        }
729
730        // Seed an existing on-disk database with a table to write into.
731        let (_dir, path) = temp_db_path("write.db");
732        {
733            let mut seed = SqliteConnection::establish(path.to_str().unwrap()).unwrap();
734            crate::sql_query("CREATE TABLE t (id INTEGER)")
735                .execute(&mut seed)
736                .unwrap();
737        }
738
739        // Disabled: the attached database is opened read-only, so writes fail.
740        conn.attach_database(path.to_str().unwrap(), "aux_write")
741            .unwrap();
742        assert!(
743            crate::sql_query("INSERT INTO aux_write.t (id) VALUES (1)")
744                .execute(conn)
745                .is_err()
746        );
747        conn.detach_database("aux_write").unwrap();
748
749        // Enabled: the attached database is writable again.
750        conn.set_attach_write_enabled(true).unwrap();
751        conn.attach_database(path.to_str().unwrap(), "aux_write")
752            .unwrap();
753        crate::sql_query("INSERT INTO aux_write.t (id) VALUES (1)")
754            .execute(conn)
755            .unwrap();
756        conn.detach_database("aux_write").unwrap();
757    }
758}