sqlx_sqlite/options/
parse.rs

1use std::borrow::Cow;
2use std::path::{Path, PathBuf};
3use std::str::FromStr;
4use std::sync::atomic::{AtomicUsize, Ordering};
5
6use percent_encoding::{percent_decode_str, percent_encode, AsciiSet};
7use url::Url;
8
9use crate::error::Error;
10use crate::SqliteConnectOptions;
11
12// https://www.sqlite.org/uri.html
13
14static IN_MEMORY_DB_SEQ: AtomicUsize = AtomicUsize::new(0);
15
16impl SqliteConnectOptions {
17    pub(crate) fn from_db_and_params(database: &str, params: Option<&str>) -> Result<Self, Error> {
18        let mut options = Self::default();
19
20        if database == ":memory:" {
21            options.in_memory = true;
22            options.shared_cache = true;
23            let seqno = IN_MEMORY_DB_SEQ.fetch_add(1, Ordering::Relaxed);
24            options.filename = Cow::Owned(PathBuf::from(format!("file:sqlx-in-memory-{seqno}")));
25        } else {
26            // % decode to allow for `?` or `#` in the filename
27            options.filename = Cow::Owned(
28                Path::new(
29                    &*percent_decode_str(database)
30                        .decode_utf8()
31                        .map_err(Error::config)?,
32                )
33                .to_path_buf(),
34            );
35        }
36
37        if let Some(params) = params {
38            for (key, value) in url::form_urlencoded::parse(params.as_bytes()) {
39                match &*key {
40                    // The mode query parameter determines if the new database is opened read-only,
41                    // read-write, read-write and created if it does not exist, or that the
42                    // database is a pure in-memory database that never interacts with disk,
43                    // respectively.
44                    "mode" => {
45                        match &*value {
46                            "ro" => {
47                                options.read_only = true;
48                            }
49
50                            // default
51                            "rw" => {}
52
53                            "rwc" => {
54                                options.create_if_missing = true;
55                            }
56
57                            "memory" => {
58                                options.in_memory = true;
59                                options.shared_cache = true;
60                            }
61
62                            _ => {
63                                return Err(Error::Configuration(
64                                    format!("unknown value {value:?} for `mode`").into(),
65                                ));
66                            }
67                        }
68                    }
69
70                    // The cache query parameter specifies the cache behaviour across multiple
71                    // connections to the same database within the process. A shared cache is
72                    // essential for persisting data across connections to an in-memory database.
73                    "cache" => match &*value {
74                        "private" => {
75                            options.shared_cache = false;
76                        }
77
78                        "shared" => {
79                            options.shared_cache = true;
80                        }
81
82                        _ => {
83                            return Err(Error::Configuration(
84                                format!("unknown value {value:?} for `cache`").into(),
85                            ));
86                        }
87                    },
88
89                    "immutable" => match &*value {
90                        "true" | "1" => {
91                            options.immutable = true;
92                        }
93                        "false" | "0" => {
94                            options.immutable = false;
95                        }
96                        _ => {
97                            return Err(Error::Configuration(
98                                format!("unknown value {value:?} for `immutable`").into(),
99                            ));
100                        }
101                    },
102
103                    "vfs" => options.vfs = Some(Cow::Owned(value.into_owned())),
104
105                    _ => {
106                        return Err(Error::Configuration(
107                            format!("unknown query parameter `{key}` while parsing connection URL")
108                                .into(),
109                        ));
110                    }
111                }
112            }
113        }
114
115        Ok(options)
116    }
117
118    pub(crate) fn build_url(&self) -> Url {
119        // https://url.spec.whatwg.org/#path-percent-encode-set
120        static PATH_ENCODE_SET: AsciiSet = percent_encoding::CONTROLS
121            .add(b' ')
122            .add(b'"')
123            .add(b'#')
124            .add(b'<')
125            .add(b'>')
126            .add(b'?')
127            .add(b'`')
128            .add(b'{')
129            .add(b'}');
130
131        let filename_encoded = percent_encode(
132            self.filename.as_os_str().as_encoded_bytes(),
133            &PATH_ENCODE_SET,
134        );
135
136        let mut url = Url::parse(&format!("sqlite://{filename_encoded}"))
137            .expect("BUG: generated un-parseable URL");
138
139        let mode = match (self.in_memory, self.create_if_missing, self.read_only) {
140            (true, _, _) => "memory",
141            (false, true, _) => "rwc",
142            (false, false, true) => "ro",
143            (false, false, false) => "rw",
144        };
145        url.query_pairs_mut().append_pair("mode", mode);
146
147        let cache = match self.shared_cache {
148            true => "shared",
149            false => "private",
150        };
151        url.query_pairs_mut().append_pair("cache", cache);
152
153        if self.immutable {
154            url.query_pairs_mut().append_pair("immutable", "true");
155        }
156
157        if let Some(vfs) = &self.vfs {
158            url.query_pairs_mut().append_pair("vfs", vfs);
159        }
160
161        url
162    }
163}
164
165impl FromStr for SqliteConnectOptions {
166    type Err = Error;
167
168    fn from_str(mut url: &str) -> Result<Self, Self::Err> {
169        // remove scheme from the URL
170        url = url
171            .trim_start_matches("sqlite://")
172            .trim_start_matches("sqlite:");
173
174        let mut database_and_params = url.splitn(2, '?');
175
176        let database = database_and_params.next().unwrap_or_default();
177        let params = database_and_params.next();
178
179        Self::from_db_and_params(database, params)
180    }
181}
182
183#[test]
184fn test_parse_in_memory() -> Result<(), Error> {
185    let options: SqliteConnectOptions = "sqlite::memory:".parse()?;
186    assert!(options.in_memory);
187    assert!(options.shared_cache);
188
189    let options: SqliteConnectOptions = "sqlite://?mode=memory".parse()?;
190    assert!(options.in_memory);
191    assert!(options.shared_cache);
192
193    let options: SqliteConnectOptions = "sqlite://:memory:".parse()?;
194    assert!(options.in_memory);
195    assert!(options.shared_cache);
196
197    let options: SqliteConnectOptions = "sqlite://?mode=memory&cache=private".parse()?;
198    assert!(options.in_memory);
199    assert!(!options.shared_cache);
200
201    Ok(())
202}
203
204#[test]
205fn test_parse_read_only() -> Result<(), Error> {
206    let options: SqliteConnectOptions = "sqlite://a.db?mode=ro".parse()?;
207    assert!(options.read_only);
208    assert_eq!(&*options.filename.to_string_lossy(), "a.db");
209
210    Ok(())
211}
212
213#[test]
214fn test_parse_shared_in_memory() -> Result<(), Error> {
215    let options: SqliteConnectOptions = "sqlite://a.db?cache=shared".parse()?;
216    assert!(options.shared_cache);
217    assert_eq!(&*options.filename.to_string_lossy(), "a.db");
218
219    Ok(())
220}
221
222#[test]
223fn it_returns_the_parsed_url() -> Result<(), Error> {
224    let url = "sqlite://test.db?mode=rw&cache=shared";
225    let options: SqliteConnectOptions = url.parse()?;
226
227    let expected_url = Url::parse(url).unwrap();
228    assert_eq!(options.build_url(), expected_url);
229
230    Ok(())
231}