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;
10mod schema;
11
12pub mod error;
13
14pub use crate::contract::{
15    AccessMode, AtomicResult, GeneratedKey, OpenOptions, Operation, OperationKind, OperationResult,
16    StorageContext, WriteResult, STORAGE_CONTEXT_VERSION,
17};
18pub use crate::error::{AdapterErrorKind, DactylError};
19pub use crate::rows::{Parameter, Row, Rows};
20pub use crate::schema::{
21    ColumnSchema, ForeignKeyAction, ForeignKeySchema, IndexSchema, StoreSchema, TableSchema,
22};
23
24use crate::adapter::Adapter;
25
26/// A supported application datastore.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum Datastore {
29    Sqlite,
30    Neon,
31}
32
33/// The route needed to send an application read or write.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct DatastoreRoute {
36    datastore: Datastore,
37    route: String,
38    token: Option<String>,
39}
40
41impl DatastoreRoute {
42    pub fn sqlite(path: impl Into<String>) -> Self {
43        Self {
44            datastore: Datastore::Sqlite,
45            route: path.into(),
46            token: None,
47        }
48    }
49
50    pub fn neon(endpoint: impl Into<String>, token: Option<String>) -> Self {
51        Self {
52            datastore: Datastore::Neon,
53            route: endpoint.into(),
54            token,
55        }
56    }
57
58    pub fn datastore(&self) -> Datastore {
59        self.datastore
60    }
61
62    pub fn route(&self) -> &str {
63        &self.route
64    }
65
66    pub fn token(&self) -> Option<&str> {
67        self.token.as_deref()
68    }
69
70    /// Resolve `DATASTORE`, `DATASTORE_ROUTE`, and `DATASTORE_TOKEN`.
71    pub fn from_env() -> Result<Self, DactylError> {
72        let datastore = std::env::var("DATASTORE")
73            .map_err(|_| DactylError::Config("DATASTORE is not set: use sqlite or neon".into()))?;
74        let route = std::env::var("DATASTORE_ROUTE")
75            .map_err(|_| DactylError::Config("DATASTORE_ROUTE is not set".into()))?;
76        match datastore.as_str() {
77            "sqlite" => Ok(Self::sqlite(route)),
78            "neon" => Ok(Self::neon(route, std::env::var("DATASTORE_TOKEN").ok())),
79            other => Err(DactylError::Config(format!(
80                "invalid DATASTORE value {other:?}: use sqlite or neon"
81            ))),
82        }
83    }
84}
85
86/// A route-scoped application driver. Backend handles remain private.
87pub struct Connection {
88    adapter: Box<dyn Adapter>,
89    route: DatastoreRoute,
90    context: Option<StorageContext>,
91}
92
93impl Connection {
94    pub fn open(route: DatastoreRoute) -> Result<Self, DactylError> {
95        Self::open_with_options_and_context(route, OpenOptions::default(), None)
96    }
97
98    pub fn open_with_options(
99        route: DatastoreRoute,
100        options: OpenOptions,
101    ) -> Result<Self, DactylError> {
102        Self::open_with_options_and_context(route, options, None)
103    }
104
105    /// Open a route with an optional caller-owned storage context.
106    ///
107    /// Local routes ignore the context. Neon routes require it for every
108    /// operation and forward it without interpreting its payload.
109    pub fn open_with_context(
110        route: DatastoreRoute,
111        context: Option<StorageContext>,
112    ) -> Result<Self, DactylError> {
113        Self::open_with_options_and_context(route, OpenOptions::default(), context)
114    }
115
116    pub fn open_with_options_and_context(
117        route: DatastoreRoute,
118        options: OpenOptions,
119        context: Option<StorageContext>,
120    ) -> Result<Self, DactylError> {
121        if let Some(context) = &context {
122            context.validate()?;
123        }
124        let adapter = build_adapter(&route, options, context.clone())?;
125        Ok(Self {
126            adapter,
127            route,
128            context,
129        })
130    }
131
132    pub fn from_env() -> Result<Self, DactylError> {
133        Self::open(DatastoreRoute::from_env()?)
134    }
135
136    pub fn datastore(&self) -> Datastore {
137        self.route.datastore
138    }
139
140    pub fn route(&self) -> &DatastoreRoute {
141        &self.route
142    }
143
144    pub fn context(&self) -> Option<&StorageContext> {
145        self.context.as_ref()
146    }
147
148    /// Read application rows from the selected backend.
149    pub fn read(&self, sql: &str, params: &[Parameter]) -> Result<Rows, DactylError> {
150        self.adapter.read(sql, params)
151    }
152
153    /// Write application data and return the explicit physical result.
154    pub fn write_result(
155        &self,
156        sql: &str,
157        params: &[Parameter],
158    ) -> Result<WriteResult, DactylError> {
159        self.adapter.write(sql, params)
160    }
161
162    /// Write application data and return the affected count for compatibility.
163    pub fn write(&self, sql: &str, params: &[Parameter]) -> Result<u64, DactylError> {
164        Ok(self.write_result(sql, params)?.affected_rows)
165    }
166
167    pub fn atomic(&self, operations: &[Operation]) -> Result<AtomicResult, DactylError> {
168        self.adapter.atomic(operations)
169    }
170
171    pub fn access_mode(&self) -> AccessMode {
172        self.adapter.access_mode()
173    }
174
175    /// Inspect the local SQLite catalog through the backend-neutral schema type.
176    pub fn inspect_schema(&self) -> Result<StoreSchema, DactylError> {
177        self.adapter.inspect_schema()
178    }
179}
180
181/// Alias that makes the application-driver role explicit.
182pub type Driver = Connection;
183
184pub fn read(sql: &str, params: &[Parameter]) -> Result<Rows, DactylError> {
185    Connection::from_env()?.read(sql, params)
186}
187
188pub fn read_with_context(
189    context: Option<StorageContext>,
190    sql: &str,
191    params: &[Parameter],
192) -> Result<Rows, DactylError> {
193    Connection::open_with_context(DatastoreRoute::from_env()?, context)?.read(sql, params)
194}
195
196pub fn write(sql: &str, params: &[Parameter]) -> Result<u64, DactylError> {
197    Connection::from_env()?.write(sql, params)
198}
199
200pub fn write_with_context(
201    context: Option<StorageContext>,
202    sql: &str,
203    params: &[Parameter],
204) -> Result<u64, DactylError> {
205    Connection::open_with_context(DatastoreRoute::from_env()?, context)?.write(sql, params)
206}
207
208#[deprecated(note = "use dactyl_db::read")]
209pub fn query(sql: &str, params: &[Parameter]) -> Result<Rows, DactylError> {
210    read(sql, params)
211}
212
213#[deprecated(note = "use dactyl_db::write")]
214pub fn execute(sql: &str, params: &[Parameter]) -> Result<u64, DactylError> {
215    write(sql, params)
216}
217
218fn build_adapter(
219    route: &DatastoreRoute,
220    _options: OpenOptions,
221    _context: Option<StorageContext>,
222) -> Result<Box<dyn Adapter>, DactylError> {
223    match route.datastore {
224        Datastore::Sqlite => {
225            #[cfg(feature = "sqlite")]
226            {
227                Ok(Box::new(
228                    crate::adapter::sqlite::SqliteAdapter::open_with_options(
229                        &route.route,
230                        _options,
231                    )?,
232                ))
233            }
234            #[cfg(not(feature = "sqlite"))]
235            {
236                Err(DactylError::Config(
237                    "sqlite support is disabled; enable the `sqlite` feature".into(),
238                ))
239            }
240        }
241        Datastore::Neon => {
242            #[cfg(feature = "neon")]
243            {
244                Ok(Box::new(
245                    crate::adapter::neon::NeonAdapter::new_with_options(
246                        &route.route,
247                        route.token.clone(),
248                        _options,
249                        _context,
250                    ),
251                ))
252            }
253            #[cfg(not(feature = "neon"))]
254            {
255                Err(DactylError::Config(
256                    "neon support is disabled; enable the `neon` feature".into(),
257                ))
258            }
259        }
260    }
261}