1mod 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum Datastore {
29 Sqlite,
30 Neon,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct DatastoreRoute {
36 datastore: Datastore,
37 route: String,
38 token: Option<String>,
39}
40
41const DATASTORE_ENV: &str = "DATASTORE";
42const DATASTORE_ROUTE_ENV: &str = "DATASTORE_ROUTE";
43const DATASTORE_TOKEN_ENV: &str = "DATASTORE_TOKEN";
44
45impl DatastoreRoute {
46 pub fn sqlite(path: impl Into<String>) -> Self {
47 Self {
48 datastore: Datastore::Sqlite,
49 route: path.into(),
50 token: None,
51 }
52 }
53
54 pub fn neon(endpoint: impl Into<String>, token: Option<String>) -> Self {
55 Self {
56 datastore: Datastore::Neon,
57 route: endpoint.into(),
58 token,
59 }
60 }
61
62 pub fn datastore(&self) -> Datastore {
63 self.datastore
64 }
65
66 pub fn route(&self) -> &str {
67 &self.route
68 }
69
70 pub fn token(&self) -> Option<&str> {
71 self.token.as_deref()
72 }
73
74 pub fn from_env() -> Result<Self, DactylError> {
81 let datastore = std::env::var(DATASTORE_ENV)
82 .map_err(|_| DactylError::Config("DATASTORE is not set: use sqlite or neon".into()))?;
83 let route = std::env::var(DATASTORE_ROUTE_ENV)
84 .map_err(|_| DactylError::Config("DATASTORE_ROUTE is not set".into()))?;
85 Self::from_env_values(
86 Some(&datastore),
87 Some(&route),
88 std::env::var(DATASTORE_TOKEN_ENV).ok().as_deref(),
89 )
90 }
91
92 fn from_env_values(
93 datastore: Option<&str>,
94 route: Option<&str>,
95 token: Option<&str>,
96 ) -> Result<Self, DactylError> {
97 let datastore = datastore.ok_or_else(|| {
98 DactylError::Config("DATASTORE is not set: use sqlite or neon".into())
99 })?;
100 let route =
101 route.ok_or_else(|| DactylError::Config("DATASTORE_ROUTE is not set".into()))?;
102 if route.trim().is_empty() {
103 return Err(DactylError::Config(
104 "DATASTORE_ROUTE must not be empty".into(),
105 ));
106 }
107
108 match datastore {
109 "sqlite" => Ok(Self::sqlite(route)),
110 "neon" => Ok(Self::neon(
111 route,
112 token
113 .filter(|value| !value.trim().is_empty())
114 .map(str::to_owned),
115 )),
116 other => Err(DactylError::Config(format!(
117 "invalid DATASTORE value {other:?}: use sqlite or neon"
118 ))),
119 }
120 }
121}
122
123pub struct Connection {
125 adapter: Box<dyn Adapter>,
126 route: DatastoreRoute,
127 context: Option<StorageContext>,
128}
129
130impl Connection {
131 pub fn open(route: DatastoreRoute) -> Result<Self, DactylError> {
132 Self::open_with_options_and_context(route, OpenOptions::default(), None)
133 }
134
135 pub fn open_with_options(
136 route: DatastoreRoute,
137 options: OpenOptions,
138 ) -> Result<Self, DactylError> {
139 Self::open_with_options_and_context(route, options, None)
140 }
141
142 pub fn open_with_context(
147 route: DatastoreRoute,
148 context: Option<StorageContext>,
149 ) -> Result<Self, DactylError> {
150 Self::open_with_options_and_context(route, OpenOptions::default(), context)
151 }
152
153 pub fn open_with_options_and_context(
154 route: DatastoreRoute,
155 options: OpenOptions,
156 context: Option<StorageContext>,
157 ) -> Result<Self, DactylError> {
158 if let Some(context) = &context {
159 context.validate()?;
160 }
161 let adapter = build_adapter(&route, options, context.clone())?;
162 Ok(Self {
163 adapter,
164 route,
165 context,
166 })
167 }
168
169 pub fn from_env() -> Result<Self, DactylError> {
170 Self::open(DatastoreRoute::from_env()?)
171 }
172
173 pub fn datastore(&self) -> Datastore {
174 self.route.datastore
175 }
176
177 pub fn route(&self) -> &DatastoreRoute {
178 &self.route
179 }
180
181 pub fn context(&self) -> Option<&StorageContext> {
182 self.context.as_ref()
183 }
184
185 pub fn read(&self, sql: &str, params: &[Parameter]) -> Result<Rows, DactylError> {
187 self.adapter.read(sql, params)
188 }
189
190 pub fn write_result(
192 &self,
193 sql: &str,
194 params: &[Parameter],
195 ) -> Result<WriteResult, DactylError> {
196 self.adapter.write(sql, params)
197 }
198
199 pub fn write(&self, sql: &str, params: &[Parameter]) -> Result<u64, DactylError> {
201 Ok(self.write_result(sql, params)?.affected_rows)
202 }
203
204 pub fn atomic(&self, operations: &[Operation]) -> Result<AtomicResult, DactylError> {
205 self.adapter.atomic(operations)
206 }
207
208 pub fn access_mode(&self) -> AccessMode {
209 self.adapter.access_mode()
210 }
211
212 pub fn inspect_schema(&self) -> Result<StoreSchema, DactylError> {
214 self.adapter.inspect_schema()
215 }
216}
217
218pub type Driver = Connection;
220
221pub fn read(sql: &str, params: &[Parameter]) -> Result<Rows, DactylError> {
222 Connection::from_env()?.read(sql, params)
223}
224
225pub fn read_with_context(
226 context: Option<StorageContext>,
227 sql: &str,
228 params: &[Parameter],
229) -> Result<Rows, DactylError> {
230 Connection::open_with_context(DatastoreRoute::from_env()?, context)?.read(sql, params)
231}
232
233pub fn write(sql: &str, params: &[Parameter]) -> Result<u64, DactylError> {
234 Connection::from_env()?.write(sql, params)
235}
236
237pub fn write_with_context(
238 context: Option<StorageContext>,
239 sql: &str,
240 params: &[Parameter],
241) -> Result<u64, DactylError> {
242 Connection::open_with_context(DatastoreRoute::from_env()?, context)?.write(sql, params)
243}
244
245#[deprecated(note = "use dactyl_db::read")]
246pub fn query(sql: &str, params: &[Parameter]) -> Result<Rows, DactylError> {
247 read(sql, params)
248}
249
250#[deprecated(note = "use dactyl_db::write")]
251pub fn execute(sql: &str, params: &[Parameter]) -> Result<u64, DactylError> {
252 write(sql, params)
253}
254
255fn build_adapter(
256 route: &DatastoreRoute,
257 _options: OpenOptions,
258 _context: Option<StorageContext>,
259) -> Result<Box<dyn Adapter>, DactylError> {
260 match route.datastore {
261 Datastore::Sqlite => {
262 #[cfg(feature = "sqlite")]
263 {
264 Ok(Box::new(
265 crate::adapter::sqlite::SqliteAdapter::open_with_options(
266 &route.route,
267 _options,
268 )?,
269 ))
270 }
271 #[cfg(not(feature = "sqlite"))]
272 {
273 Err(DactylError::Config(
274 "sqlite support is disabled; enable the `sqlite` feature".into(),
275 ))
276 }
277 }
278 Datastore::Neon => {
279 #[cfg(feature = "neon")]
280 {
281 Ok(Box::new(
282 crate::adapter::neon::NeonAdapter::new_with_options(
283 &route.route,
284 route.token.clone(),
285 _options,
286 _context,
287 ),
288 ))
289 }
290 #[cfg(not(feature = "neon"))]
291 {
292 Err(DactylError::Config(
293 "neon support is disabled; enable the `neon` feature".into(),
294 ))
295 }
296 }
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::{Datastore, DatastoreRoute};
303
304 #[test]
305 fn ambient_route_requires_selector_and_non_empty_route() {
306 assert!(DatastoreRoute::from_env_values(None, Some("/tmp/app.db"), None).is_err());
307 assert!(DatastoreRoute::from_env_values(Some("sqlite"), None, None).is_err());
308 assert!(DatastoreRoute::from_env_values(Some("sqlite"), Some(" "), None).is_err());
309 assert!(DatastoreRoute::from_env_values(Some("unknown"), Some("route"), None).is_err());
310 }
311
312 #[test]
313 fn ambient_selector_controls_backend_and_token_is_neon_only() {
314 let sqlite =
315 DatastoreRoute::from_env_values(Some("sqlite"), Some("/tmp/app.db"), Some("secret"))
316 .unwrap();
317 assert_eq!(sqlite.datastore(), Datastore::Sqlite);
318 assert_eq!(sqlite.route(), "/tmp/app.db");
319 assert_eq!(sqlite.token(), None);
320
321 let neon = DatastoreRoute::from_env_values(
322 Some("neon"),
323 Some("https://propodus.example"),
324 Some("secret"),
325 )
326 .unwrap();
327 assert_eq!(neon.datastore(), Datastore::Neon);
328 assert_eq!(neon.route(), "https://propodus.example");
329 assert_eq!(neon.token(), Some("secret"));
330
331 let blank_token = DatastoreRoute::from_env_values(
332 Some("neon"),
333 Some("https://propodus.example"),
334 Some(" "),
335 )
336 .unwrap();
337 assert_eq!(blank_token.token(), None);
338 }
339}