1mod adapter;
10mod rows;
11
12pub mod error;
13pub mod query;
14
15#[doc(hidden)]
16pub mod __private;
17
18pub use dactyl_db_macros::query;
19
20pub use crate::error::DactylError;
21pub use crate::query::{Construct, Dialect, QueryAnalyzer};
22pub use crate::rows::{Parameter, Row, Rows};
23
24use crate::adapter::Adapter;
25use std::time::Duration;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum Datastore {
30 Sqlite,
31 Neon,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum SqliteJournalMode {
38 Wal,
39 Delete,
40 Memory,
41 Off,
42}
43
44impl SqliteJournalMode {
45 #[cfg(feature = "sqlite")]
46 pub(crate) fn as_sql(self) -> &'static str {
47 match self {
48 Self::Wal => "WAL",
49 Self::Delete => "DELETE",
50 Self::Memory => "MEMORY",
51 Self::Off => "OFF",
52 }
53 }
54}
55
56impl Datastore {
57 fn dialect(self) -> Dialect {
58 match self {
59 Datastore::Sqlite => Dialect::Sqlite,
60 Datastore::Neon => Dialect::Postgres,
61 }
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct DatastoreRoute {
68 datastore: Datastore,
69 route: String,
70 token: Option<String>,
71}
72
73impl DatastoreRoute {
74 pub fn sqlite(path: impl Into<String>) -> Self {
76 Self {
77 datastore: Datastore::Sqlite,
78 route: path.into(),
79 token: None,
80 }
81 }
82
83 pub fn neon(endpoint: impl Into<String>, token: Option<String>) -> Self {
85 Self {
86 datastore: Datastore::Neon,
87 route: endpoint.into(),
88 token,
89 }
90 }
91
92 pub fn datastore(&self) -> Datastore {
93 self.datastore
94 }
95
96 pub fn route(&self) -> &str {
97 &self.route
98 }
99
100 pub fn token(&self) -> Option<&str> {
101 self.token.as_deref()
102 }
103
104 pub fn from_env() -> Result<Self, DactylError> {
107 let datastore = std::env::var("DATASTORE").map_err(|_| {
108 DactylError::Adapter("DATASTORE is not set: set DATASTORE and DATASTORE_ROUTE".into())
109 })?;
110 let kind = match datastore.as_str() {
111 "sqlite" => Datastore::Sqlite,
112 "neon" => Datastore::Neon,
113 other => Err(DactylError::Adapter(format!(
114 "invalid DATASTORE value {other:?}: must be 'sqlite' or 'neon'"
115 )))?,
116 };
117 let route = std::env::var("DATASTORE_ROUTE").map_err(|_| {
118 DactylError::Adapter(
119 "DATASTORE_ROUTE is not set: set DATASTORE and DATASTORE_ROUTE".into(),
120 )
121 })?;
122 Ok(match kind {
123 Datastore::Sqlite => Self::sqlite(route),
124 Datastore::Neon => Self::neon(route, std::env::var("DATASTORE_TOKEN").ok()),
125 })
126 }
127}
128
129#[derive(Debug, Clone)]
131pub struct ConnectionOptions {
132 pub allow_rewrites: bool,
134 pub read_only: bool,
136 pub busy_timeout: Duration,
138 pub foreign_keys: bool,
140 pub journal_mode: Option<SqliteJournalMode>,
143}
144
145impl Default for ConnectionOptions {
146 fn default() -> Self {
147 Self {
148 allow_rewrites: false,
149 read_only: false,
150 busy_timeout: Duration::from_secs(5),
151 foreign_keys: true,
152 journal_mode: Some(SqliteJournalMode::Wal),
153 }
154 }
155}
156
157impl ConnectionOptions {
158 pub fn with_rewrites(mut self, allow: bool) -> Self {
159 self.allow_rewrites = allow;
160 self
161 }
162
163 pub fn read_only(mut self, read_only: bool) -> Self {
164 self.read_only = read_only;
165 self
166 }
167
168 fn from_env() -> Self {
169 let allow_rewrites = std::env::var("DATASTORE_REWRITE")
170 .ok()
171 .map(|value| {
172 matches!(
173 value.to_ascii_lowercase().as_str(),
174 "1" | "true" | "yes" | "on"
175 )
176 })
177 .unwrap_or(false);
178 Self {
179 allow_rewrites,
180 ..Self::default()
181 }
182 }
183}
184
185pub struct Connection {
187 adapter: Box<dyn Adapter>,
188 route: DatastoreRoute,
189 options: ConnectionOptions,
190}
191
192impl Connection {
193 pub fn open(route: DatastoreRoute) -> Result<Self, DactylError> {
195 Self::open_with_options(route, ConnectionOptions::default())
196 }
197
198 #[cfg(any(feature = "sqlite", feature = "neon"))]
200 pub fn open_with_options(
201 route: DatastoreRoute,
202 options: ConnectionOptions,
203 ) -> Result<Self, DactylError> {
204 let adapter: Box<dyn Adapter> = match route.datastore {
205 Datastore::Sqlite => {
206 #[cfg(feature = "sqlite")]
207 {
208 Box::new(
209 crate::adapter::sqlite::SqliteAdapter::open_with_options(
210 &route.route,
211 options.read_only,
212 options.busy_timeout,
213 options.foreign_keys,
214 options.journal_mode,
215 )
216 .map_err(|e| DactylError::Adapter(format!("sqlite open: {e}")))?,
217 )
218 }
219 #[cfg(not(feature = "sqlite"))]
220 {
221 return Err(DactylError::Adapter(
222 "sqlite adapter requested but `sqlite` feature is disabled".into(),
223 ));
224 }
225 }
226 Datastore::Neon => {
227 #[cfg(feature = "neon")]
228 {
229 Box::new(crate::adapter::neon::NeonAdapter::new(
230 &route.route,
231 route.token.clone(),
232 ))
233 }
234 #[cfg(not(feature = "neon"))]
235 {
236 return Err(DactylError::Adapter(
237 "neon adapter requested but `neon` feature is disabled".into(),
238 ));
239 }
240 }
241 };
242 Ok(Self {
243 adapter,
244 route,
245 options,
246 })
247 }
248
249 #[cfg(not(any(feature = "sqlite", feature = "neon")))]
251 pub fn open_with_options(
252 _route: DatastoreRoute,
253 _options: ConnectionOptions,
254 ) -> Result<Self, DactylError> {
255 Err(DactylError::Adapter(
256 "no datastore adapter feature is enabled; enable `sqlite` or `neon`".into(),
257 ))
258 }
259
260 pub fn from_env() -> Result<Self, DactylError> {
262 Self::open_with_options(DatastoreRoute::from_env()?, ConnectionOptions::from_env())
263 }
264
265 pub fn datastore(&self) -> Datastore {
266 self.route.datastore
267 }
268
269 pub fn dialect(&self) -> Dialect {
270 self.route.datastore.dialect()
271 }
272
273 pub fn route(&self) -> &DatastoreRoute {
274 &self.route
275 }
276
277 pub fn options(&self) -> &ConnectionOptions {
278 &self.options
279 }
280
281 pub fn query(&self, sql: &str, params: &[Parameter]) -> Result<Rows, DactylError> {
283 let prepared = self.prepare(sql)?;
284 self.adapter.execute(&prepared, params)
285 }
286
287 pub fn execute(&self, sql: &str, params: &[Parameter]) -> Result<u64, DactylError> {
289 let prepared = self.prepare(sql)?;
290 self.adapter.execute_raw(&prepared, params)
291 }
292
293 pub fn execute_batch(&self, sql: &str) -> Result<(), DactylError> {
295 let prepared = self.prepare(sql)?;
296 self.adapter.execute_script(&prepared)
297 }
298
299 pub fn transaction(&self, statements: &[Statement]) -> Result<Vec<Rows>, DactylError> {
301 if statements.is_empty() {
302 return Ok(Vec::new());
303 }
304 let prepared = statements
305 .iter()
306 .map(|statement| {
307 Ok(Statement {
308 sql: self.prepare(&statement.sql)?,
309 params: statement.params.clone(),
310 })
311 })
312 .collect::<Result<Vec<_>, DactylError>>()?;
313 self.adapter.execute_batch(&prepared)
314 }
315
316 pub fn last_insert_id(&self) -> Result<i64, DactylError> {
318 self.adapter.last_insert_id()
319 }
320
321 pub fn execute_op(&self, op: StorageOp) -> Result<StorageResult, DactylError> {
325 match op {
326 StorageOp::Query { sql, params } => self.query(&sql, ¶ms).map(StorageResult::Rows),
327 StorageOp::Execute { sql, params } => {
328 self.execute(&sql, ¶ms).map(StorageResult::Affected)
329 }
330 StorageOp::Script { sql } => self.execute_batch(&sql).map(|()| StorageResult::Unit),
331 StorageOp::Transaction { statements } => {
332 self.transaction(&statements).map(StorageResult::Batch)
333 }
334 StorageOp::LastInsertId => self.last_insert_id().map(StorageResult::LastInsertId),
335 }
336 }
337
338 fn prepare(&self, sql: &str) -> Result<String, DactylError> {
339 QueryAnalyzer::new().prepare(sql, self.dialect(), self.options.allow_rewrites)
340 }
341}
342
343#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
345pub enum StorageOp {
346 Query { sql: String, params: Vec<Parameter> },
347 Execute { sql: String, params: Vec<Parameter> },
348 Script { sql: String },
349 Transaction { statements: Vec<Statement> },
350 LastInsertId,
351}
352
353#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
355pub enum StorageResult {
356 Rows(Rows),
357 Affected(u64),
358 Unit,
359 Batch(Vec<Rows>),
360 LastInsertId(i64),
361}
362
363#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
365pub struct Statement {
366 pub sql: String,
367 pub params: Vec<Parameter>,
368}
369
370impl Statement {
371 pub fn new(sql: &str, params: Vec<Parameter>) -> Self {
372 Self {
373 sql: sql.to_string(),
374 params,
375 }
376 }
377}
378
379#[doc(hidden)]
382pub fn reset() {}
383
384fn connection_for_query(sql: &str) -> Result<Connection, DactylError> {
385 let route = route_for_query(sql)?;
386 Connection::open_with_options(route, ConnectionOptions::from_env())
387}
388
389fn route_for_query(sql: &str) -> Result<DatastoreRoute, DactylError> {
390 let active = DatastoreRoute::from_env()?;
391 let analyzed = QueryAnalyzer::new().analyze(sql);
392 let Some(inline) = analyzed.inline_override else {
393 return Ok(active);
394 };
395 let Some(inline_datastore) = (match inline {
396 "sqlite" => Some(Datastore::Sqlite),
397 "neon" => Some(Datastore::Neon),
398 _ => None,
399 }) else {
400 return Err(DactylError::Routing(format!(
401 "unknown inline datastore {inline:?}"
402 )));
403 };
404 if inline_datastore == active.datastore {
405 return Ok(active);
406 }
407
408 let (route_var, token_var) = match inline_datastore {
409 Datastore::Sqlite => ("DATASTORE_SQLITE_ROUTE", None),
410 Datastore::Neon => ("DATASTORE_NEON_ROUTE", Some("DATASTORE_NEON_TOKEN")),
411 };
412 let route = std::env::var(route_var).map_err(|_| {
413 DactylError::Routing(format!(
414 "inline datastore {inline:?} requires {route_var} when it differs from DATASTORE"
415 ))
416 })?;
417 let token = token_var
418 .and_then(|name| std::env::var(name).ok())
419 .or_else(|| {
420 (inline_datastore == Datastore::Neon)
421 .then(|| std::env::var("DATASTORE_TOKEN").ok())
422 .flatten()
423 });
424 Ok(match inline_datastore {
425 Datastore::Sqlite => DatastoreRoute::sqlite(route),
426 Datastore::Neon => DatastoreRoute::neon(route, token),
427 })
428}
429
430pub fn query(sql: &str, params: &[Parameter]) -> Result<Rows, DactylError> {
432 connection_for_query(sql)?.query(sql, params)
433}
434
435pub fn execute(sql: &str, params: &[Parameter]) -> Result<u64, DactylError> {
437 connection_for_query(sql)?.execute(sql, params)
438}
439
440pub fn execute_batch(sql: &str) -> Result<(), DactylError> {
442 connection_for_query(sql)?.execute_batch(sql)
443}
444
445pub fn transaction(statements: &[Statement]) -> Result<Vec<Rows>, DactylError> {
447 if statements.is_empty() {
448 return Ok(Vec::new());
449 }
450 connection_for_query(&statements[0].sql)?.transaction(statements)
451}