sqlx_build_trust_sqlite/options/
parse.rs

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