Skip to main content

dactyl_db/
lib.rs

1//! Dactyl — a lightweight Rust storage driver for local and Neon routes.
2//!
3//! Dactyl owns backend selection, parameter binding, response normalization,
4//! physical atomic batches, access mode, and local durability. It does not own
5//! schema policy, migration ids/order, retries, analytics, or business logic.
6
7mod adapter;
8mod contract;
9mod rows;
10
11pub mod error;
12
13pub use crate::contract::{
14    AccessMode, AtomicResult, GeneratedKey, OpenOptions, Operation, OperationKind, OperationResult,
15    WriteResult,
16};
17pub use crate::error::{AdapterErrorKind, DactylError};
18pub use crate::rows::{Parameter, Row, Rows};
19
20use crate::adapter::Adapter;
21
22/// A supported application datastore.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum Datastore {
25    Sqlite,
26    Neon,
27}
28
29/// The route needed to send an application read or write.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct DatastoreRoute {
32    datastore: Datastore,
33    route: String,
34    token: Option<String>,
35}
36
37impl DatastoreRoute {
38    pub fn sqlite(path: impl Into<String>) -> Self {
39        Self {
40            datastore: Datastore::Sqlite,
41            route: path.into(),
42            token: None,
43        }
44    }
45
46    pub fn neon(endpoint: impl Into<String>, token: Option<String>) -> Self {
47        Self {
48            datastore: Datastore::Neon,
49            route: endpoint.into(),
50            token,
51        }
52    }
53
54    pub fn datastore(&self) -> Datastore {
55        self.datastore
56    }
57
58    pub fn route(&self) -> &str {
59        &self.route
60    }
61
62    pub fn token(&self) -> Option<&str> {
63        self.token.as_deref()
64    }
65
66    /// Resolve `DATASTORE`, `DATASTORE_ROUTE`, and `DATASTORE_TOKEN`.
67    pub fn from_env() -> Result<Self, DactylError> {
68        let datastore = std::env::var("DATASTORE")
69            .map_err(|_| DactylError::Config("DATASTORE is not set: use sqlite or neon".into()))?;
70        let route = std::env::var("DATASTORE_ROUTE")
71            .map_err(|_| DactylError::Config("DATASTORE_ROUTE is not set".into()))?;
72        match datastore.as_str() {
73            "sqlite" => Ok(Self::sqlite(route)),
74            "neon" => Ok(Self::neon(route, std::env::var("DATASTORE_TOKEN").ok())),
75            other => Err(DactylError::Config(format!(
76                "invalid DATASTORE value {other:?}: use sqlite or neon"
77            ))),
78        }
79    }
80}
81
82/// A route-scoped application driver. Backend handles remain private.
83pub struct Connection {
84    adapter: Box<dyn Adapter>,
85    route: DatastoreRoute,
86}
87
88impl Connection {
89    pub fn open(route: DatastoreRoute) -> Result<Self, DactylError> {
90        Self::open_with_options(route, OpenOptions::default())
91    }
92
93    pub fn open_with_options(
94        route: DatastoreRoute,
95        options: OpenOptions,
96    ) -> Result<Self, DactylError> {
97        let adapter = build_adapter(&route, options)?;
98        Ok(Self { adapter, route })
99    }
100
101    pub fn from_env() -> Result<Self, DactylError> {
102        Self::open(DatastoreRoute::from_env()?)
103    }
104
105    pub fn datastore(&self) -> Datastore {
106        self.route.datastore
107    }
108
109    pub fn route(&self) -> &DatastoreRoute {
110        &self.route
111    }
112
113    /// Read application rows from the selected backend.
114    pub fn read(&self, sql: &str, params: &[Parameter]) -> Result<Rows, DactylError> {
115        self.adapter.read(sql, params)
116    }
117
118    /// Write application data and return the explicit physical result.
119    pub fn write_result(
120        &self,
121        sql: &str,
122        params: &[Parameter],
123    ) -> Result<WriteResult, DactylError> {
124        self.adapter.write(sql, params)
125    }
126
127    /// Write application data and return the affected count for compatibility.
128    pub fn write(&self, sql: &str, params: &[Parameter]) -> Result<u64, DactylError> {
129        Ok(self.write_result(sql, params)?.affected_rows)
130    }
131
132    pub fn atomic(&self, operations: &[Operation]) -> Result<AtomicResult, DactylError> {
133        self.adapter.atomic(operations)
134    }
135
136    pub fn access_mode(&self) -> AccessMode {
137        self.adapter.access_mode()
138    }
139}
140
141/// Alias that makes the application-driver role explicit.
142pub type Driver = Connection;
143
144pub fn read(sql: &str, params: &[Parameter]) -> Result<Rows, DactylError> {
145    Connection::from_env()?.read(sql, params)
146}
147
148pub fn write(sql: &str, params: &[Parameter]) -> Result<u64, DactylError> {
149    Connection::from_env()?.write(sql, params)
150}
151
152#[deprecated(note = "use dactyl_db::read")]
153pub fn query(sql: &str, params: &[Parameter]) -> Result<Rows, DactylError> {
154    read(sql, params)
155}
156
157#[deprecated(note = "use dactyl_db::write")]
158pub fn execute(sql: &str, params: &[Parameter]) -> Result<u64, DactylError> {
159    write(sql, params)
160}
161
162fn build_adapter(
163    route: &DatastoreRoute,
164    options: OpenOptions,
165) -> Result<Box<dyn Adapter>, DactylError> {
166    match route.datastore {
167        Datastore::Sqlite => {
168            #[cfg(feature = "sqlite")]
169            {
170                Ok(Box::new(
171                    crate::adapter::sqlite::SqliteAdapter::open_with_options(
172                        &route.route,
173                        options,
174                    )?,
175                ))
176            }
177            #[cfg(not(feature = "sqlite"))]
178            {
179                Err(DactylError::Config(
180                    "sqlite support is disabled; enable the `sqlite` feature".into(),
181                ))
182            }
183        }
184        Datastore::Neon => {
185            #[cfg(feature = "neon")]
186            {
187                Ok(Box::new(
188                    crate::adapter::neon::NeonAdapter::new_with_options(
189                        &route.route,
190                        route.token.clone(),
191                        options,
192                    ),
193                ))
194            }
195            #[cfg(not(feature = "neon"))]
196            {
197                Err(DactylError::Config(
198                    "neon support is disabled; enable the `neon` feature".into(),
199                ))
200            }
201        }
202    }
203}