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    StorageContext, WriteResult, STORAGE_CONTEXT_VERSION,
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    context: Option<StorageContext>,
87}
88
89impl Connection {
90    pub fn open(route: DatastoreRoute) -> Result<Self, DactylError> {
91        Self::open_with_options_and_context(route, OpenOptions::default(), None)
92    }
93
94    pub fn open_with_options(
95        route: DatastoreRoute,
96        options: OpenOptions,
97    ) -> Result<Self, DactylError> {
98        Self::open_with_options_and_context(route, options, None)
99    }
100
101    /// Open a route with an optional caller-owned storage context.
102    ///
103    /// Local routes ignore the context. Neon routes require it for every
104    /// operation and forward it without interpreting its payload.
105    pub fn open_with_context(
106        route: DatastoreRoute,
107        context: Option<StorageContext>,
108    ) -> Result<Self, DactylError> {
109        Self::open_with_options_and_context(route, OpenOptions::default(), context)
110    }
111
112    pub fn open_with_options_and_context(
113        route: DatastoreRoute,
114        options: OpenOptions,
115        context: Option<StorageContext>,
116    ) -> Result<Self, DactylError> {
117        if let Some(context) = &context {
118            context.validate()?;
119        }
120        let adapter = build_adapter(&route, options, context.clone())?;
121        Ok(Self {
122            adapter,
123            route,
124            context,
125        })
126    }
127
128    pub fn from_env() -> Result<Self, DactylError> {
129        Self::open(DatastoreRoute::from_env()?)
130    }
131
132    pub fn datastore(&self) -> Datastore {
133        self.route.datastore
134    }
135
136    pub fn route(&self) -> &DatastoreRoute {
137        &self.route
138    }
139
140    pub fn context(&self) -> Option<&StorageContext> {
141        self.context.as_ref()
142    }
143
144    /// Read application rows from the selected backend.
145    pub fn read(&self, sql: &str, params: &[Parameter]) -> Result<Rows, DactylError> {
146        self.adapter.read(sql, params)
147    }
148
149    /// Write application data and return the explicit physical result.
150    pub fn write_result(
151        &self,
152        sql: &str,
153        params: &[Parameter],
154    ) -> Result<WriteResult, DactylError> {
155        self.adapter.write(sql, params)
156    }
157
158    /// Write application data and return the affected count for compatibility.
159    pub fn write(&self, sql: &str, params: &[Parameter]) -> Result<u64, DactylError> {
160        Ok(self.write_result(sql, params)?.affected_rows)
161    }
162
163    pub fn atomic(&self, operations: &[Operation]) -> Result<AtomicResult, DactylError> {
164        self.adapter.atomic(operations)
165    }
166
167    pub fn access_mode(&self) -> AccessMode {
168        self.adapter.access_mode()
169    }
170}
171
172/// Alias that makes the application-driver role explicit.
173pub type Driver = Connection;
174
175pub fn read(sql: &str, params: &[Parameter]) -> Result<Rows, DactylError> {
176    Connection::from_env()?.read(sql, params)
177}
178
179pub fn read_with_context(
180    context: Option<StorageContext>,
181    sql: &str,
182    params: &[Parameter],
183) -> Result<Rows, DactylError> {
184    Connection::open_with_context(DatastoreRoute::from_env()?, context)?.read(sql, params)
185}
186
187pub fn write(sql: &str, params: &[Parameter]) -> Result<u64, DactylError> {
188    Connection::from_env()?.write(sql, params)
189}
190
191pub fn write_with_context(
192    context: Option<StorageContext>,
193    sql: &str,
194    params: &[Parameter],
195) -> Result<u64, DactylError> {
196    Connection::open_with_context(DatastoreRoute::from_env()?, context)?.write(sql, params)
197}
198
199#[deprecated(note = "use dactyl_db::read")]
200pub fn query(sql: &str, params: &[Parameter]) -> Result<Rows, DactylError> {
201    read(sql, params)
202}
203
204#[deprecated(note = "use dactyl_db::write")]
205pub fn execute(sql: &str, params: &[Parameter]) -> Result<u64, DactylError> {
206    write(sql, params)
207}
208
209fn build_adapter(
210    route: &DatastoreRoute,
211    _options: OpenOptions,
212    _context: Option<StorageContext>,
213) -> Result<Box<dyn Adapter>, DactylError> {
214    match route.datastore {
215        Datastore::Sqlite => {
216            #[cfg(feature = "sqlite")]
217            {
218                Ok(Box::new(
219                    crate::adapter::sqlite::SqliteAdapter::open_with_options(
220                        &route.route,
221                        _options,
222                    )?,
223                ))
224            }
225            #[cfg(not(feature = "sqlite"))]
226            {
227                Err(DactylError::Config(
228                    "sqlite support is disabled; enable the `sqlite` feature".into(),
229                ))
230            }
231        }
232        Datastore::Neon => {
233            #[cfg(feature = "neon")]
234            {
235                Ok(Box::new(
236                    crate::adapter::neon::NeonAdapter::new_with_options(
237                        &route.route,
238                        route.token.clone(),
239                        _options,
240                        _context,
241                    ),
242                ))
243            }
244            #[cfg(not(feature = "neon"))]
245            {
246                Err(DactylError::Config(
247                    "neon support is disabled; enable the `neon` feature".into(),
248                ))
249            }
250        }
251    }
252}