1use std::path::PathBuf;
2use std::time::Duration;
3
4use crate::{Error, Result};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Driver {
9 Mysql,
11 Pgsql,
13 Sqlite,
15}
16
17impl Driver {
18 fn parse(value: &str) -> Result<Self> {
19 match value {
20 "mariadb" | "mysql" => Ok(Self::Mysql),
21 "pgsql" | "postgres" | "postgresql" => Ok(Self::Pgsql),
22 "sqlite" => Ok(Self::Sqlite),
23 _ => Err(Error::invalid_url(format!("unknown driver `{value}`"))),
24 }
25 }
26}
27
28#[derive(Debug, Clone, Default)]
39pub struct ConnectOptions {
40 pub(crate) driver: Option<Driver>,
41 pub(crate) host: Option<String>,
42 pub(crate) port: Option<u16>,
43 pub(crate) username: Option<String>,
44 pub(crate) password: Option<String>,
45 pub(crate) database: Option<String>,
46 pub(crate) unix_socket: Option<String>,
47 pub(crate) path: Option<PathBuf>,
48 pub(crate) in_memory: bool,
49 pub(crate) read_only: bool,
50 pub(crate) create_if_missing: bool,
51 pub(crate) busy_timeout: Option<Duration>,
52}
53
54impl ConnectOptions {
55 pub fn new(driver: Driver) -> Self {
57 Self {
58 driver: Some(driver),
59 create_if_missing: true,
60 ..Self::default()
61 }
62 }
63
64 pub fn driver(mut self, value: Driver) -> Self {
66 self.driver = Some(value);
67 self
68 }
69
70 pub fn host(mut self, value: impl Into<String>) -> Self {
74 self.host = Some(value.into());
75 self
76 }
77
78 pub fn port(mut self, value: u16) -> Self {
80 self.port = Some(value);
81 self
82 }
83
84 pub fn username(mut self, value: impl Into<String>) -> Self {
86 self.username = Some(value.into());
87 self
88 }
89
90 pub fn password(mut self, value: impl Into<String>) -> Self {
92 self.password = Some(value.into());
93 self
94 }
95
96 pub fn database(mut self, value: impl Into<String>) -> Self {
98 self.database = Some(value.into());
99 self
100 }
101
102 pub fn unix_socket(mut self, value: impl Into<String>) -> Self {
104 self.unix_socket = Some(value.into());
105 self
106 }
107
108 pub fn path(mut self, value: impl Into<PathBuf>) -> Self {
110 self.path = Some(value.into());
111 self
112 }
113
114 pub fn in_memory(mut self) -> Self {
118 self.in_memory = true;
119 self.path = None;
120 self
121 }
122
123 pub fn read_only(mut self, value: bool) -> Self {
125 self.read_only = value;
126 self
127 }
128
129 pub fn create_if_missing(mut self, value: bool) -> Self {
131 self.create_if_missing = value;
132 self
133 }
134
135 pub fn busy_timeout(mut self, value: Duration) -> Self {
137 self.busy_timeout = Some(value);
138 self
139 }
140
141 pub fn from_url(input: &str) -> Result<Self> {
146 if input == "sqlite::memory:" {
147 return Ok(Self::new(Driver::Sqlite).in_memory());
148 }
149
150 if let Some(path) = input.strip_prefix("sqlite:") {
151 return parse_sqlite_url(path);
152 }
153
154 let url = ParsedUrl::parse(input)?;
155 let driver = Driver::parse(&url.scheme)?;
156
157 match driver {
158 Driver::Mysql => {
159 let mut options = Self::new(Driver::Mysql);
160 if let Some(host) = url.host.as_deref() {
161 options = options.host(host);
162 }
163 if let Some(port) = url.port {
164 options = options.port(port);
165 }
166 if let Some(username) = url.username.as_deref() {
167 if !username.is_empty() {
168 options = options.username(username);
169 }
170 }
171 if let Some(password) = url.password.as_deref() {
172 options = options.password(password);
173 }
174 let database = url.path.trim_start_matches('/');
175 if !database.is_empty() {
176 options = options.database(database);
177 }
178 Ok(options)
179 }
180 Driver::Pgsql => {
181 let mut options = Self::new(Driver::Pgsql);
182 if let Some(host) = url.host.as_deref() {
183 options = options.host(host);
184 }
185 if let Some(port) = url.port {
186 options = options.port(port);
187 }
188 if let Some(username) = url.username.as_deref() {
189 if !username.is_empty() {
190 options = options.username(username);
191 }
192 }
193 if let Some(password) = url.password.as_deref() {
194 options = options.password(password);
195 }
196 let database = url.path.trim_start_matches('/');
197 if !database.is_empty() {
198 options = options.database(database);
199 }
200 Ok(options)
201 }
202 Driver::Sqlite => unreachable!("sqlite URLs are handled before generic parsing"),
203 }
204 }
205}
206
207#[derive(Debug, Clone, Default)]
209pub struct MysqlConnectOptions {
210 pub(crate) host: Option<String>,
211 pub(crate) port: Option<u32>,
212 pub(crate) username: Option<String>,
213 pub(crate) password: Option<String>,
214 pub(crate) database: Option<String>,
215 pub(crate) unix_socket: Option<String>,
216}
217
218impl MysqlConnectOptions {
219 pub fn new() -> Self {
221 Self::default()
222 }
223
224 pub fn host(mut self, value: impl Into<String>) -> Self {
226 self.host = Some(value.into());
227 self
228 }
229
230 pub fn port(mut self, value: u32) -> Self {
232 self.port = Some(value);
233 self
234 }
235
236 pub fn username(mut self, value: impl Into<String>) -> Self {
238 self.username = Some(value.into());
239 self
240 }
241
242 pub fn password(mut self, value: impl Into<String>) -> Self {
244 self.password = Some(value.into());
245 self
246 }
247
248 pub fn database(mut self, value: impl Into<String>) -> Self {
250 self.database = Some(value.into());
251 self
252 }
253
254 pub fn unix_socket(mut self, value: impl Into<String>) -> Self {
256 self.unix_socket = Some(value.into());
257 self
258 }
259}
260
261#[cfg(feature = "mariadb")]
262impl From<MysqlConnectOptions> for quex_driver::mysql::ConnectOptions {
263 fn from(value: MysqlConnectOptions) -> Self {
264 let mut options = quex_driver::mysql::ConnectOptions::new();
265 if let Some(host) = value.host {
266 options = options.host(host);
267 }
268 if let Some(port) = value.port {
269 options = options.port(port);
270 }
271 if let Some(username) = value.username {
272 options = options.user(username);
273 }
274 if let Some(password) = value.password {
275 options = options.password(password);
276 }
277 if let Some(database) = value.database {
278 options = options.database(database);
279 }
280 if let Some(unix_socket) = value.unix_socket {
281 options = options.unix_socket(unix_socket);
282 }
283 options
284 }
285}
286
287impl From<MysqlConnectOptions> for ConnectOptions {
288 fn from(value: MysqlConnectOptions) -> Self {
289 let mut options = Self::new(Driver::Mysql);
290 if let Some(host) = value.host {
291 options = options.host(host);
292 }
293 if let Some(port) = value.port {
294 options = options.port(port as u16);
295 }
296 if let Some(username) = value.username {
297 options = options.username(username);
298 }
299 if let Some(password) = value.password {
300 options = options.password(password);
301 }
302 if let Some(database) = value.database {
303 options = options.database(database);
304 }
305 if let Some(unix_socket) = value.unix_socket {
306 options = options.unix_socket(unix_socket);
307 }
308 options
309 }
310}
311
312#[derive(Debug, Clone, Default)]
314pub struct PostgresConnectOptions {
315 pub(crate) host: Option<String>,
316 pub(crate) port: Option<u16>,
317 pub(crate) username: Option<String>,
318 pub(crate) password: Option<String>,
319 pub(crate) database: Option<String>,
320}
321
322impl PostgresConnectOptions {
323 pub fn new() -> Self {
325 Self::default()
326 }
327
328 pub fn host(mut self, value: impl Into<String>) -> Self {
330 self.host = Some(value.into());
331 self
332 }
333
334 pub fn port(mut self, value: u16) -> Self {
336 self.port = Some(value);
337 self
338 }
339
340 pub fn username(mut self, value: impl Into<String>) -> Self {
342 self.username = Some(value.into());
343 self
344 }
345
346 pub fn password(mut self, value: impl Into<String>) -> Self {
348 self.password = Some(value.into());
349 self
350 }
351
352 pub fn database(mut self, value: impl Into<String>) -> Self {
354 self.database = Some(value.into());
355 self
356 }
357}
358
359#[cfg(feature = "postgres")]
360impl From<PostgresConnectOptions> for quex_driver::postgres::ConnectOptions {
361 fn from(value: PostgresConnectOptions) -> Self {
362 let mut options = quex_driver::postgres::ConnectOptions::new();
363 if let Some(host) = value.host {
364 options = options.host(host);
365 }
366 if let Some(port) = value.port {
367 options = options.port(port);
368 }
369 if let Some(username) = value.username {
370 options = options.user(username);
371 }
372 if let Some(password) = value.password {
373 options = options.password(password);
374 }
375 if let Some(database) = value.database {
376 options = options.database(database);
377 }
378 options
379 }
380}
381
382impl From<PostgresConnectOptions> for ConnectOptions {
383 fn from(value: PostgresConnectOptions) -> Self {
384 let mut options = Self::new(Driver::Pgsql);
385 if let Some(host) = value.host {
386 options = options.host(host);
387 }
388 if let Some(port) = value.port {
389 options = options.port(port);
390 }
391 if let Some(username) = value.username {
392 options = options.username(username);
393 }
394 if let Some(password) = value.password {
395 options = options.password(password);
396 }
397 if let Some(database) = value.database {
398 options = options.database(database);
399 }
400 options
401 }
402}
403
404#[derive(Debug, Clone, Default)]
406pub struct SqliteConnectOptions {
407 pub(crate) path: Option<PathBuf>,
408 pub(crate) in_memory: bool,
409 pub(crate) read_only: bool,
410 pub(crate) create_if_missing: bool,
411 pub(crate) busy_timeout: Option<Duration>,
412}
413
414impl SqliteConnectOptions {
415 pub fn new() -> Self {
417 Self {
418 create_if_missing: true,
419 ..Self::default()
420 }
421 }
422
423 pub fn path(mut self, value: impl Into<PathBuf>) -> Self {
425 self.path = Some(value.into());
426 self
427 }
428
429 pub fn in_memory(mut self) -> Self {
433 self.in_memory = true;
434 self.path = None;
435 self
436 }
437
438 pub fn read_only(mut self, value: bool) -> Self {
440 self.read_only = value;
441 self
442 }
443
444 pub fn create_if_missing(mut self, value: bool) -> Self {
446 self.create_if_missing = value;
447 self
448 }
449
450 pub fn busy_timeout(mut self, value: Duration) -> Self {
452 self.busy_timeout = Some(value);
453 self
454 }
455}
456
457#[cfg(feature = "sqlite")]
458impl From<SqliteConnectOptions> for quex_driver::sqlite::ConnectOptions {
459 fn from(value: SqliteConnectOptions) -> Self {
460 let mut options = quex_driver::sqlite::ConnectOptions::new()
461 .read_only(value.read_only)
462 .create_if_missing(value.create_if_missing);
463 if let Some(timeout) = value.busy_timeout {
464 options = options.busy_timeout(timeout);
465 }
466 if value.in_memory {
467 options = options.in_memory();
468 } else if let Some(path) = value.path {
469 options = options.path(path);
470 }
471 options
472 }
473}
474
475impl From<SqliteConnectOptions> for ConnectOptions {
476 fn from(value: SqliteConnectOptions) -> Self {
477 let mut options = Self::new(Driver::Sqlite)
478 .read_only(value.read_only)
479 .create_if_missing(value.create_if_missing);
480 if let Some(timeout) = value.busy_timeout {
481 options = options.busy_timeout(timeout);
482 }
483 if value.in_memory {
484 options = options.in_memory();
485 } else if let Some(path) = value.path {
486 options = options.path(path);
487 }
488 options
489 }
490}
491
492fn parse_sqlite_url(rest: &str) -> Result<ConnectOptions> {
493 if rest == ":memory:" {
494 return Ok(ConnectOptions::new(Driver::Sqlite).in_memory());
495 }
496 if rest.is_empty() {
497 return Err(Error::invalid_url("sqlite URL is missing a database path"));
498 }
499 if let Some(path) = rest.strip_prefix("///") {
500 return Ok(ConnectOptions::new(Driver::Sqlite).path(format!("/{}", path)));
501 }
502 if let Some(path) = rest.strip_prefix("//") {
503 if path.is_empty() {
504 return Err(Error::invalid_url("sqlite URL is missing a database path"));
505 }
506 return Ok(ConnectOptions::new(Driver::Sqlite).path(path));
507 }
508 Ok(ConnectOptions::new(Driver::Sqlite).path(rest))
509}
510
511#[derive(Debug)]
512struct ParsedUrl {
513 scheme: String,
514 username: Option<String>,
515 password: Option<String>,
516 host: Option<String>,
517 port: Option<u16>,
518 path: String,
519}
520
521impl ParsedUrl {
522 fn parse(input: &str) -> Result<Self> {
523 let (scheme, remainder) = input
524 .split_once("://")
525 .ok_or_else(|| Error::invalid_url("URL is missing a scheme separator"))?;
526 let scheme = scheme.to_ascii_lowercase();
527
528 let (authority, path_and_more) = remainder.split_once('/').unwrap_or((remainder, ""));
529 let path = if path_and_more.is_empty() {
530 String::new()
531 } else {
532 let path = format!("/{path_and_more}");
533 strip_suffixes(&path, &['?', '#']).to_owned()
534 };
535
536 let (username, password, host, port) = parse_authority(authority)?;
537
538 Ok(Self {
539 scheme,
540 username,
541 password,
542 host,
543 port,
544 path,
545 })
546 }
547}
548
549fn parse_authority(
550 authority: &str,
551) -> Result<(Option<String>, Option<String>, Option<String>, Option<u16>)> {
552 let (userinfo, host_port) = if let Some((userinfo, host_port)) = authority.rsplit_once('@') {
553 (Some(userinfo), host_port)
554 } else {
555 (None, authority)
556 };
557
558 let (username, password) = if let Some(userinfo) = userinfo {
559 if let Some((username, password)) = userinfo.split_once(':') {
560 (Some(username.to_owned()), Some(password.to_owned()))
561 } else {
562 (Some(userinfo.to_owned()), None)
563 }
564 } else {
565 (None, None)
566 };
567
568 let (host, port) = parse_host_port(host_port)?;
569 Ok((username, password, host, port))
570}
571
572fn parse_host_port(input: &str) -> Result<(Option<String>, Option<u16>)> {
573 if input.is_empty() {
574 return Ok((None, None));
575 }
576
577 if let Some(host) = input.strip_prefix('[') {
578 let (host, remainder) = host
579 .split_once(']')
580 .ok_or_else(|| Error::invalid_url("invalid IPv6 host"))?;
581 let port = if remainder.is_empty() {
582 None
583 } else if let Some(port) = remainder.strip_prefix(':') {
584 Some(parse_port(port)?)
585 } else {
586 return Err(Error::invalid_url("invalid host and port"));
587 };
588
589 return Ok((Some(host.to_owned()), port));
590 }
591
592 if let Some((host, port)) = input.rsplit_once(':') {
593 if host.contains(':') {
594 return Ok((Some(input.to_owned()), None));
595 }
596
597 return Ok((Some(host.to_owned()), Some(parse_port(port)?)));
598 }
599
600 Ok((Some(input.to_owned()), None))
601}
602
603fn parse_port(input: &str) -> Result<u16> {
604 input
605 .parse()
606 .map_err(|_| Error::invalid_url(format!("invalid port `{input}`")))
607}
608
609fn strip_suffixes<'a>(input: &'a str, separators: &[char]) -> &'a str {
610 input
611 .find(|c| separators.contains(&c))
612 .map(|index| &input[..index])
613 .unwrap_or(input)
614}