Skip to main content

slave_framework/
database.rs

1//! Connections to the application's [Turso Cloud](https://turso.tech) database.
2//!
3//! Only libSQL's remote client is compiled (HTTP, pure Rust): there is no
4//! database engine in the application. Configure it with `TURSO_DATABASE_URL`
5//! (e.g. `libsql://shop-acme.turso.io`) and `TURSO_AUTH_TOKEN`.
6
7use std::sync::Arc;
8
9use libsql::{Builder, Connection};
10
11use crate::{Error, Result};
12
13/// Environment variable holding the database URL.
14pub const URL_ENV: &str = "TURSO_DATABASE_URL";
15/// Environment variable holding the database auth token.
16pub const TOKEN_ENV: &str = "TURSO_AUTH_TOKEN";
17
18/// How to get a database URL and token, shown when they are missing.
19pub const SETUP_HINT: &str = "Create a database with `turso db create <name>`, then set \
20    TURSO_DATABASE_URL (`turso db show --url <name>`) and TURSO_AUTH_TOKEN \
21    (`turso db tokens create <name>`) in .env.";
22
23/// Where to connect: a database URL and its auth token.
24#[derive(Clone)]
25pub struct DatabaseConfig {
26    pub url: String,
27    pub auth_token: String,
28}
29
30impl std::fmt::Debug for DatabaseConfig {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        f.debug_struct("DatabaseConfig")
33            .field("url", &self.url)
34            .field("auth_token", &"<redacted>")
35            .finish()
36    }
37}
38
39impl DatabaseConfig {
40    /// Reads `TURSO_DATABASE_URL` (required) and `TURSO_AUTH_TOKEN` (empty
41    /// for servers without authentication, such as `turso dev`).
42    pub fn from_env() -> Result<Self> {
43        let url = std::env::var(URL_ENV)
44            .ok()
45            .filter(|url| !url.trim().is_empty())
46            .ok_or(Error::MissingDatabaseUrl(URL_ENV))?;
47        let auth_token = std::env::var(TOKEN_ENV).unwrap_or_default();
48        Ok(Self { url, auth_token })
49    }
50}
51
52/// A shareable handle to the database. Cheap to clone.
53///
54/// Open one per process and call [`connect`](Database::connect) per unit of
55/// work (for example, per request).
56#[derive(Clone)]
57pub struct Database {
58    inner: Arc<libsql::Database>,
59    url: Arc<str>,
60}
61
62impl std::fmt::Debug for Database {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        f.debug_struct("Database").field("url", &self.url).finish()
65    }
66}
67
68impl Database {
69    /// Prepares a client for the database at `config.url`. No request is
70    /// made until the first query.
71    pub async fn open(config: &DatabaseConfig) -> Result<Self> {
72        let inner = Builder::new_remote(config.url.clone(), config.auth_token.clone())
73            .build()
74            .await?;
75        Ok(Self {
76            inner: Arc::new(inner),
77            url: config.url.as_str().into(),
78        })
79    }
80
81    /// [`open`](Self::open) with [`DatabaseConfig::from_env`].
82    pub async fn from_env() -> Result<Self> {
83        Self::open(&DatabaseConfig::from_env()?).await
84    }
85
86    /// The database URL (never the token), for logs and messages.
87    pub fn url(&self) -> &str {
88        &self.url
89    }
90
91    /// Opens a new connection with foreign-key enforcement enabled.
92    pub async fn connect(&self) -> Result<Connection, libsql::Error> {
93        let conn = self.inner.connect()?;
94        conn.execute("PRAGMA foreign_keys = ON", ()).await?;
95        Ok(conn)
96    }
97}