runlight_sqlx/lib.rs
1//! Runlight's tables in the app's own database, through sqlx and the app's
2//! own pool.
3//!
4//! ```no_run
5//! # #[cfg(feature = "sqlite")]
6//! # async fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
7//! use sqlx::sqlite::{SqliteConnectOptions, SqlitePool};
8//!
9//! let pool = SqlitePool::connect_with(SqliteConnectOptions::new().filename("data/runlight.db").create_if_missing(true)).await?;
10//! let store = runlight_sqlx::sqlite(pool);
11//! # Ok(())
12//! # }
13//! ```
14//!
15//! The tables are the TypeScript SDK's, statement for statement, so a Rust
16//! process can share a database with a Node or PHP one. Each database is a
17//! feature of the crate: `sqlite`, `postgres`, and `mysql` (MySQL 8.0.20 or
18//! MariaDB 11.4 and later).
19//!
20//! Values reach the database as the TypeScript drivers send them. SQLite
21//! binds them typed, as better-sqlite3 does. Postgres and MySQL get them in
22//! the statement's text, as node-postgres's untyped parameters and mysql2's
23//! client-side escaping do, so every statement means the same to the server
24//! as it does from Node: a value Postgres would infer the type of is written
25//! as a literal it infers the type of in the same way.
26
27#![forbid(unsafe_code)]
28
29#[cfg(feature = "mysql")]
30mod mysql;
31#[cfg(feature = "postgres")]
32mod postgres;
33mod rows;
34#[cfg(feature = "sqlite")]
35mod sqlite;
36
37#[cfg(feature = "mysql")]
38pub use mysql::MysqlDb;
39#[cfg(feature = "postgres")]
40pub use postgres::PostgresDb;
41#[cfg(feature = "sqlite")]
42pub use sqlite::SqliteDb;
43
44use runlight::store::{DbError, PREFIX, SqlStore, check_prefix};
45#[cfg(any(feature = "sqlite", feature = "postgres", feature = "mysql"))]
46use std::sync::Arc;
47
48/// Runlight's tables in a SQLite database, on one connection of the app's pool, which the store
49/// holds for its statements in turn (an in-memory database is one per connection, and SQLite has one
50/// writer at a time anyway). The connection is put in WAL mode with a busy timeout of five seconds.
51#[cfg(feature = "sqlite")]
52pub fn sqlite(pool: sqlx::SqlitePool) -> SqlStore {
53 SqlStore::new(Arc::new(SqliteDb::new(pool)))
54}
55
56/// [`sqlite`], with every table and index name starting with `prefix` in place of `rl_`, so two apps
57/// sharing one database each keep a set of their own: up to 32 letters, digits, and underscores, not
58/// starting with a digit.
59#[cfg(feature = "sqlite")]
60pub fn sqlite_with_prefix(pool: sqlx::SqlitePool, prefix: &str) -> Result<SqlStore, DbError> {
61 SqlStore::with_prefix(Arc::new(SqliteDb::new(pool)), prefix)
62}
63
64/// Runlight's tables in Postgres, on the app's pool. The pool needs at least two connections: some
65/// work holds one while it reads through another.
66#[cfg(feature = "postgres")]
67pub fn postgres(pool: sqlx::PgPool) -> SqlStore {
68 SqlStore::new(Arc::new(PostgresDb::new(pool)))
69}
70
71/// [`postgres`], with every table and index name starting with `prefix` in place of `rl_`, so two apps
72/// sharing one database each keep a set of their own: up to 32 letters, digits, and underscores, not
73/// starting with a digit.
74#[cfg(feature = "postgres")]
75pub fn postgres_with_prefix(pool: sqlx::PgPool, prefix: &str) -> Result<SqlStore, DbError> {
76 SqlStore::with_prefix(Arc::new(PostgresDb::new(pool)), prefix)
77}
78
79/// Runlight's tables in MySQL 8.0.20 or MariaDB 11.4 and later, on the app's pool, which needs at least
80/// two connections. Text is utf8mb4 with a binary collation, so it compares and sorts by code point,
81/// as SQLite and Postgres do. Values are written into each statement escaped with backslashes, so each
82/// statement first takes NO_BACKSLASH_ESCAPES out of its connection's sql_mode.
83#[cfg(feature = "mysql")]
84pub fn mysql(pool: sqlx::MySqlPool) -> SqlStore {
85 SqlStore::new(Arc::new(MysqlDb::new(pool)))
86}
87
88/// [`mysql`], with every table and index name starting with `prefix` in place of `rl_`, so two apps
89/// sharing one database each keep a set of their own: up to 32 letters, digits, and underscores, not
90/// starting with a digit.
91#[cfg(feature = "mysql")]
92pub fn mysql_with_prefix(pool: sqlx::MySqlPool, prefix: &str) -> Result<SqlStore, DbError> {
93 SqlStore::with_prefix(Arc::new(MysqlDb::new(pool)), prefix)
94}
95
96/// A store from a connection URL: `sqlite:` (or a path), `postgres://`, or `mysql://` and `mariadb://`,
97/// with a pool of its own, as the standalone server and the CLI open one. A new SQLite file is made for
98/// its owner and group alone (0660).
99pub async fn connect(url: &str) -> Result<SqlStore, DbError> {
100 connect_with_prefix(url, PREFIX).await
101}
102
103/// [`connect`], with every table and index name starting with `prefix` in place of `rl_`, so two apps
104/// sharing one database each keep a set of their own: up to 32 letters, digits, and underscores, not
105/// starting with a digit.
106pub async fn connect_with_prefix(url: &str, prefix: &str) -> Result<SqlStore, DbError> {
107 check_prefix(prefix)?;
108 let fail = |e: &dyn std::fmt::Display| DbError(e.to_string());
109 let lower = url.to_ascii_lowercase();
110 if lower.starts_with("postgres://") || lower.starts_with("postgresql://") {
111 #[cfg(feature = "postgres")]
112 {
113 let pool = sqlx::postgres::PgPoolOptions::new()
114 .max_connections(10)
115 .acquire_timeout(std::time::Duration::from_secs(10))
116 .connect(url)
117 .await
118 .map_err(|e| fail(&e))?;
119 return postgres_with_prefix(pool, prefix);
120 }
121 #[cfg(not(feature = "postgres"))]
122 return Err(fail(&"Runlight: built without the postgres feature"));
123 }
124 if lower.starts_with("mysql://") || lower.starts_with("mariadb://") {
125 #[cfg(feature = "mysql")]
126 {
127 let url = format!("mysql://{}", &url[url.find("://").map_or(0, |i| i + 3)..]);
128 let pool = sqlx::mysql::MySqlPoolOptions::new()
129 .max_connections(10)
130 .acquire_timeout(std::time::Duration::from_secs(10))
131 .connect(&url)
132 .await
133 .map_err(|e| fail(&e))?;
134 return mysql_with_prefix(pool, prefix);
135 }
136 #[cfg(not(feature = "mysql"))]
137 return Err(fail(&"Runlight: built without the mysql feature"));
138 }
139 #[cfg(feature = "sqlite")]
140 {
141 use std::str::FromStr;
142 let path = url.strip_prefix("sqlite://").or_else(|| url.strip_prefix("sqlite:")).unwrap_or(url);
143 let options = if path == ":memory:" {
144 sqlx::sqlite::SqliteConnectOptions::from_str("sqlite::memory:").map_err(|e| fail(&e))?
145 } else {
146 if !path.starts_with("file:") {
147 sqlite::own_file(std::path::Path::new(path));
148 }
149 sqlx::sqlite::SqliteConnectOptions::new().filename(path).create_if_missing(true)
150 };
151 let pool = sqlx::sqlite::SqlitePoolOptions::new()
152 .max_connections(1)
153 .connect_with(options)
154 .await
155 .map_err(|e| fail(&e))?;
156 sqlite_with_prefix(pool, prefix)
157 }
158 #[cfg(not(feature = "sqlite"))]
159 Err(fail(&"Runlight: built without the sqlite feature"))
160}