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::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    fn db_config_fts3_tokenizer_roundtrip() {
434        let conn = &mut connection();
435        conn.set_fts3_tokenizer_enabled(false).unwrap();
436        assert!(!conn.is_fts3_tokenizer_enabled().unwrap());
437        conn.set_fts3_tokenizer_enabled(true).unwrap();
438        assert!(conn.is_fts3_tokenizer_enabled().unwrap());
439    }
440
441    #[diesel_test_helper::test]
442    fn db_config_writable_schema_roundtrip() {
443        let conn = &mut connection();
444        conn.set_writable_schema(false).unwrap();
445        assert!(!conn.is_writable_schema().unwrap());
446        conn.set_writable_schema(true).unwrap();
447        assert!(conn.is_writable_schema().unwrap());
448    }
449
450    #[diesel_test_helper::test]
451    fn db_config_attach_create_roundtrip() {
452        let conn = &mut connection();
453        if conn.version() < DatabaseVersion::new(3, 49, 0) {
454            return;
455        }
456        conn.set_attach_create_enabled(false).unwrap();
457        assert!(!conn.is_attach_create_enabled().unwrap());
458        conn.set_attach_create_enabled(true).unwrap();
459        assert!(conn.is_attach_create_enabled().unwrap());
460    }
461
462    #[diesel_test_helper::test]
463    fn db_config_attach_write_roundtrip() {
464        let conn = &mut connection();
465        if conn.version() < DatabaseVersion::new(3, 49, 0) {
466            return;
467        }
468        conn.set_attach_write_enabled(false).unwrap();
469        assert!(!conn.is_attach_write_enabled().unwrap());
470        conn.set_attach_write_enabled(true).unwrap();
471        assert!(conn.is_attach_write_enabled().unwrap());
472    }
473
474    // ---- behavioral db_config tests ----
475
476    #[diesel_test_helper::test]
477    fn defensive_mode_blocks_writable_schema() {
478        let conn = &mut connection();
479        conn.set_defensive(true).unwrap();
480        // In defensive mode, writable_schema should remain off even if we try to set it
481        let _ = crate::sql_query("PRAGMA writable_schema = ON").execute(conn);
482        assert!(!conn.is_writable_schema().unwrap());
483    }
484
485    #[diesel_test_helper::test]
486    fn foreign_keys_enabled_enforces_constraints() {
487        let conn = &mut connection();
488        conn.set_foreign_keys_enabled(true).unwrap();
489
490        crate::sql_query("CREATE TABLE parent (id INTEGER PRIMARY KEY)")
491            .execute(conn)
492            .unwrap();
493        crate::sql_query(
494            "CREATE TABLE child (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent(id))",
495        )
496        .execute(conn)
497        .unwrap();
498
499        if cfg!(not(miri)) {
500            // fii string access
501            // Insert a child row with no matching parent — should fail with FK enabled
502            let result =
503                crate::sql_query("INSERT INTO child (id, parent_id) VALUES (1, 999)").execute(conn);
504            assert!(result.is_err());
505        }
506    }
507
508    #[diesel_test_helper::test]
509    fn views_disabled_blocks_view_queries() {
510        let conn = &mut connection();
511        crate::sql_query("CREATE TABLE base (id INTEGER PRIMARY KEY)")
512            .execute(conn)
513            .unwrap();
514        crate::sql_query("INSERT INTO base (id) VALUES (1)")
515            .execute(conn)
516            .unwrap();
517        crate::sql_query("CREATE VIEW base_view AS SELECT id FROM base")
518            .execute(conn)
519            .unwrap();
520
521        // Enabled (default): the view can be queried.
522        conn.set_views_enabled(true).unwrap();
523        assert!(
524            crate::sql_query("SELECT id FROM base_view")
525                .execute(conn)
526                .is_ok()
527        );
528
529        if cfg!(not(miri)) {
530            // ffi string access
531            // Disabled: queries that reference the view fail.
532            conn.set_views_enabled(false).unwrap();
533            assert!(
534                crate::sql_query("SELECT id FROM base_view")
535                    .execute(conn)
536                    .is_err()
537            );
538        }
539    }
540
541    #[diesel_test_helper::test]
542    fn triggers_disabled_prevents_firing() {
543        let conn = &mut connection();
544        crate::sql_query("CREATE TABLE source (id INTEGER PRIMARY KEY)")
545            .execute(conn)
546            .unwrap();
547        crate::sql_query("CREATE TABLE trigger_log (n INTEGER)")
548            .execute(conn)
549            .unwrap();
550        crate::sql_query("CREATE TRIGGER log_insert AFTER INSERT ON source BEGIN INSERT INTO trigger_log (n) VALUES (1); END")
551            .execute(conn)
552            .unwrap();
553
554        // Disabled: inserting into `source` must not fire the trigger.
555        conn.set_triggers_enabled(false).unwrap();
556        crate::sql_query("INSERT INTO source (id) VALUES (1)")
557            .execute(conn)
558            .unwrap();
559        let count: i64 = sql::<crate::sql_types::BigInt>("SELECT COUNT(*) FROM trigger_log")
560            .get_result(conn)
561            .unwrap();
562        assert_eq!(0, count, "trigger should not fire while disabled");
563
564        // Enabled: the trigger fires and writes one row.
565        conn.set_triggers_enabled(true).unwrap();
566        crate::sql_query("INSERT INTO source (id) VALUES (2)")
567            .execute(conn)
568            .unwrap();
569        let count: i64 = sql::<crate::sql_types::BigInt>("SELECT COUNT(*) FROM trigger_log")
570            .get_result(conn)
571            .unwrap();
572        assert_eq!(1, count, "trigger should fire while enabled");
573    }
574
575    #[cfg(not(miri))] // ffi string access
576    #[diesel_test_helper::test]
577    fn dqs_dml_controls_double_quoted_string_literals() {
578        let conn = &mut connection();
579
580        // Disabled: a double-quoted token in DML is parsed as an identifier, so a
581        // bare `"text"` that is not a column errors.
582        conn.set_double_quoted_strings_dml(false).unwrap();
583
584        let disabled = sql::<Text>(r#"SELECT "bare_token""#).get_result::<String>(conn);
585        assert!(disabled.is_err());
586
587        // Enabled: the same token is accepted as a string literal.
588        conn.set_double_quoted_strings_dml(true).unwrap();
589        let enabled = sql::<Text>(r#"SELECT "bare_token""#).get_result::<String>(conn);
590        assert_eq!(Ok("bare_token".to_owned()), enabled);
591    }
592
593    #[diesel_test_helper::test]
594    fn dqs_ddl_controls_double_quoted_string_literals() {
595        let conn = &mut connection();
596
597        // Disabled: a double-quoted token in a CHECK constraint is parsed as an
598        // identifier. As there is no such column, creating the table errors.
599        conn.set_double_quoted_strings_ddl(false).unwrap();
600        // accesses an ffi allocated string
601        if cfg!(not(miri)) {
602            let disabled = crate::sql_query(
603                r#"CREATE TABLE dqs_off (name TEXT, CHECK (name <> "not_a_column"))"#,
604            )
605            .execute(conn);
606            assert!(disabled.is_err());
607        }
608
609        // Enabled: the same token is accepted as a string literal, so the CHECK
610        // constraint (and the table) are created successfully.
611        conn.set_double_quoted_strings_ddl(true).unwrap();
612        let enabled =
613            crate::sql_query(r#"CREATE TABLE dqs_on (name TEXT, CHECK (name <> "not_a_column"))"#)
614                .execute(conn);
615        assert!(enabled.is_ok());
616    }
617
618    #[diesel_test_helper::test]
619    fn writable_schema_controls_direct_sqlite_master_writes() {
620        let conn = &mut connection();
621        crate::sql_query("CREATE TABLE protected (id INTEGER PRIMARY KEY)")
622            .execute(conn)
623            .unwrap();
624
625        let update =
626            "UPDATE sqlite_master SET sql = sql WHERE type = 'table' AND name = 'protected'";
627
628        if cfg!(not(miri)) {
629            // ffi string access
630            // Disabled (default): a direct write to sqlite_master is rejected.
631            conn.set_writable_schema(false).unwrap();
632            assert!(crate::sql_query(update).execute(conn).is_err());
633        }
634
635        // Enabled: the same write is permitted.
636        conn.set_writable_schema(true).unwrap();
637        assert!(crate::sql_query(update).execute(conn).is_ok());
638    }
639
640    #[cfg(not(miri))] // potential ffi string access
641    #[diesel_test_helper::test]
642    fn fts3_tokenizer_disabled_blocks_the_function() {
643        let conn = &mut connection();
644
645        // Enable first to detect whether FTS3 is compiled into this SQLite build.
646        conn.set_fts3_tokenizer_enabled(true).unwrap();
647        let enabled = sql::<crate::sql_types::Binary>("SELECT fts3_tokenizer('simple')")
648            .get_result::<Vec<u8>>(conn);
649        if enabled.is_err() {
650            // FTS3 is not available in this build, so there is nothing to assert.
651            return;
652        }
653
654        // Disabled: the `fts3_tokenizer()` SQL function is no longer callable.
655        conn.set_fts3_tokenizer_enabled(false).unwrap();
656        let disabled = sql::<crate::sql_types::Binary>("SELECT fts3_tokenizer('simple')")
657            .get_result::<Vec<u8>>(conn);
658        assert!(disabled.is_err());
659    }
660
661    // These ATTACH tests need a real filesystem (temp files), which is not
662    // available on the wasm target, where SQLite is in-memory only.
663    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
664    fn temp_db_path(name: &str) -> (tempfile::TempDir, std::path::PathBuf) {
665        let dir = tempfile::tempdir().unwrap();
666        let path = dir.path().join(name);
667        (dir, path)
668    }
669
670    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
671    #[diesel_test_helper::test]
672    fn attach_create_disabled_blocks_new_database_files() {
673        let conn = &mut connection();
674
675        // The ATTACH_CREATE option was added in SQLite 3.49.0; skip on older
676        // libraries (e.g. the system SQLite on the Ubuntu 24.04 CI runners).
677        if conn.set_attach_create_enabled(false).is_err() {
678            return;
679        }
680
681        let (_dir, path) = temp_db_path("create.db");
682
683        // Disabled: attaching a path that does not exist yet must fail.
684        assert!(
685            conn.attach_database(path.to_str().unwrap(), "aux_create")
686                .is_err()
687        );
688
689        // Enabled: the same ATTACH now creates and opens the file.
690        conn.set_attach_create_enabled(true).unwrap();
691        conn.attach_database(path.to_str().unwrap(), "aux_create")
692            .unwrap();
693        conn.detach_database("aux_create").unwrap();
694    }
695
696    #[cfg(not(any(all(target_family = "wasm", target_os = "unknown"), miri)))]
697    #[diesel_test_helper::test]
698    fn attach_write_disabled_opens_attached_databases_read_only() {
699        let conn = &mut connection();
700
701        // The ATTACH_WRITE option was added in SQLite 3.49.0; skip on older
702        // libraries (e.g. the system SQLite on the Ubuntu 24.04 CI runners).
703        // This guard also leaves ATTACH_WRITE disabled for the first check below.
704        if conn.set_attach_write_enabled(false).is_err() {
705            return;
706        }
707
708        // Seed an existing on-disk database with a table to write into.
709        let (_dir, path) = temp_db_path("write.db");
710        {
711            let mut seed = SqliteConnection::establish(path.to_str().unwrap()).unwrap();
712            crate::sql_query("CREATE TABLE t (id INTEGER)")
713                .execute(&mut seed)
714                .unwrap();
715        }
716
717        // Disabled: the attached database is opened read-only, so writes fail.
718        conn.attach_database(path.to_str().unwrap(), "aux_write")
719            .unwrap();
720        assert!(
721            crate::sql_query("INSERT INTO aux_write.t (id) VALUES (1)")
722                .execute(conn)
723                .is_err()
724        );
725        conn.detach_database("aux_write").unwrap();
726
727        // Enabled: the attached database is writable again.
728        conn.set_attach_write_enabled(true).unwrap();
729        conn.attach_database(path.to_str().unwrap(), "aux_write")
730            .unwrap();
731        crate::sql_query("INSERT INTO aux_write.t (id) VALUES (1)")
732            .execute(conn)
733            .unwrap();
734        conn.detach_database("aux_write").unwrap();
735    }
736}