Skip to main content

dactyl_db/
lib.rs

1//! Dactyl — the application-layer read/write driver for SQLite and Neon.
2//!
3//! Dactyl owns only backend selection, parameter binding, and response
4//! normalization. It forwards raw application SQL to the selected database;
5//! schema administration, migrations, transactions, analytics, retries, and
6//! business intelligence stay outside this crate.
7
8mod adapter;
9mod rows;
10
11pub mod error;
12
13pub use crate::error::{AdapterErrorKind, DactylError};
14pub use crate::rows::{Parameter, Row, Rows};
15
16use crate::adapter::Adapter;
17
18/// A supported application datastore.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Datastore {
21    Sqlite,
22    Neon,
23}
24
25/// The route needed to send an application read or write.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct DatastoreRoute {
28    datastore: Datastore,
29    route: String,
30    token: Option<String>,
31}
32
33impl DatastoreRoute {
34    pub fn sqlite(path: impl Into<String>) -> Self {
35        Self {
36            datastore: Datastore::Sqlite,
37            route: path.into(),
38            token: None,
39        }
40    }
41
42    pub fn neon(endpoint: impl Into<String>, token: Option<String>) -> Self {
43        Self {
44            datastore: Datastore::Neon,
45            route: endpoint.into(),
46            token,
47        }
48    }
49
50    pub fn datastore(&self) -> Datastore {
51        self.datastore
52    }
53
54    pub fn route(&self) -> &str {
55        &self.route
56    }
57
58    pub fn token(&self) -> Option<&str> {
59        self.token.as_deref()
60    }
61
62    /// Resolve `DATASTORE`, `DATASTORE_ROUTE`, and `DATASTORE_TOKEN`.
63    pub fn from_env() -> Result<Self, DactylError> {
64        let datastore = std::env::var("DATASTORE")
65            .map_err(|_| DactylError::Config("DATASTORE is not set: use sqlite or neon".into()))?;
66        let route = std::env::var("DATASTORE_ROUTE")
67            .map_err(|_| DactylError::Config("DATASTORE_ROUTE is not set".into()))?;
68        match datastore.as_str() {
69            "sqlite" => Ok(Self::sqlite(route)),
70            "neon" => Ok(Self::neon(route, std::env::var("DATASTORE_TOKEN").ok())),
71            other => Err(DactylError::Config(format!(
72                "invalid DATASTORE value {other:?}: use sqlite or neon"
73            ))),
74        }
75    }
76}
77
78/// A route-scoped application driver. Backend handles remain private.
79pub struct Connection {
80    adapter: Box<dyn Adapter>,
81    route: DatastoreRoute,
82}
83
84impl Connection {
85    pub fn open(route: DatastoreRoute) -> Result<Self, DactylError> {
86        let adapter = build_adapter(&route)?;
87        Ok(Self { adapter, route })
88    }
89
90    pub fn from_env() -> Result<Self, DactylError> {
91        Self::open(DatastoreRoute::from_env()?)
92    }
93
94    pub fn datastore(&self) -> Datastore {
95        self.route.datastore
96    }
97
98    pub fn route(&self) -> &DatastoreRoute {
99        &self.route
100    }
101
102    /// Read application rows from the selected backend.
103    pub fn read(&self, sql: &str, params: &[Parameter]) -> Result<Rows, DactylError> {
104        self.adapter.read(sql, params)
105    }
106
107    /// Write application data and return the backend-reported affected count.
108    pub fn write(&self, sql: &str, params: &[Parameter]) -> Result<u64, DactylError> {
109        self.adapter.write(sql, params)
110    }
111}
112
113/// Alias that makes the application-driver role explicit.
114pub type Driver = Connection;
115
116pub fn read(sql: &str, params: &[Parameter]) -> Result<Rows, DactylError> {
117    Connection::from_env()?.read(sql, params)
118}
119
120pub fn write(sql: &str, params: &[Parameter]) -> Result<u64, DactylError> {
121    Connection::from_env()?.write(sql, params)
122}
123
124#[deprecated(note = "use dactyl_db::read")]
125pub fn query(sql: &str, params: &[Parameter]) -> Result<Rows, DactylError> {
126    read(sql, params)
127}
128
129#[deprecated(note = "use dactyl_db::write")]
130pub fn execute(sql: &str, params: &[Parameter]) -> Result<u64, DactylError> {
131    write(sql, params)
132}
133
134fn build_adapter(route: &DatastoreRoute) -> Result<Box<dyn Adapter>, DactylError> {
135    match route.datastore {
136        Datastore::Sqlite => {
137            #[cfg(feature = "sqlite")]
138            {
139                Ok(Box::new(crate::adapter::sqlite::SqliteAdapter::open(
140                    &route.route,
141                )?))
142            }
143            #[cfg(not(feature = "sqlite"))]
144            {
145                Err(DactylError::Config(
146                    "sqlite support is disabled; enable the `sqlite` feature".into(),
147                ))
148            }
149        }
150        Datastore::Neon => {
151            #[cfg(feature = "neon")]
152            {
153                Ok(Box::new(crate::adapter::neon::NeonAdapter::new(
154                    &route.route,
155                    route.token.clone(),
156                )))
157            }
158            #[cfg(not(feature = "neon"))]
159            {
160                Err(DactylError::Config(
161                    "neon support is disabled; enable the `neon` feature".into(),
162                ))
163            }
164        }
165    }
166}