sqlx_sqlite/options/
parse.rs1use 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
12static 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 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 "mode" => {
45 match &*value {
46 "ro" => {
47 options.read_only = true;
48 }
49
50 "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 "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 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 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}