1pub mod adapter;
7pub mod error;
8pub mod query;
9mod rows;
10
11#[doc(hidden)]
12pub mod __private;
13
14pub use dactyl_db_macros::query;
15
16pub use crate::error::DactylError;
17pub use crate::rows::{Parameter, Row, Rows};
18
19use std::sync::{Arc, Mutex, OnceLock};
20
21use crate::adapter::Adapter;
22
23#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
25pub struct Statement {
26 pub sql: String,
27 pub params: Vec<Parameter>,
28}
29
30impl Statement {
31 pub fn new(sql: &str, params: Vec<Parameter>) -> Self {
33 Self {
34 sql: sql.to_string(),
35 params,
36 }
37 }
38}
39
40static CONNECTIONS: OnceLock<Mutex<std::collections::HashMap<String, Arc<dyn Adapter>>>> =
43 OnceLock::new();
44
45fn connections() -> &'static Mutex<std::collections::HashMap<String, Arc<dyn Adapter>>> {
46 CONNECTIONS.get_or_init(|| Mutex::new(std::collections::HashMap::new()))
47}
48
49#[doc(hidden)]
52pub fn __reset_for_tests() {
53 let mut guard = connections()
54 .lock()
55 .expect("dactyl connection lock poisoned");
56 guard.clear();
57}
58
59pub fn init(datastore: &str, route: &str, token: Option<&str>) {
64 std::env::set_var("DATASTORE", datastore);
65 std::env::set_var("DATASTORE_ROUTE", route);
66 if let Some(t) = token {
67 std::env::set_var("DATASTORE_TOKEN", t);
68 } else {
69 std::env::remove_var("DATASTORE_TOKEN");
70 }
71}
72
73pub fn reset() {
75 __reset_for_tests();
76}
77
78pub fn read(query: &str, params: &[Parameter], optimize: bool) -> Result<Rows, DactylError> {
80 dispatch(query, params, optimize, false)
81}
82
83pub fn write(query: &str, params: &[Parameter], optimize: bool) -> Result<Rows, DactylError> {
85 dispatch(query, params, optimize, true)
86}
87
88pub fn execute(query: &str, params: &[Parameter]) -> Result<u64, DactylError> {
90 validate_env()?;
91 let analyzer = query::QueryAnalyzer::new();
92 let analyzed = analyzer.analyze(query);
93 let key = connection_key().ok_or_else(|| {
94 DactylError::Adapter("no adapter configured: set DATASTORE and DATASTORE_ROUTE".into())
95 })?;
96 let adapter = connection(&key, query, &analyzed)?;
97 let sql = analyzed.rewrite.apply(query);
98 adapter.execute_raw(&sql, params)
99}
100
101pub fn transaction(statements: &[Statement]) -> Result<Vec<Rows>, DactylError> {
103 if statements.is_empty() {
104 return Ok(Vec::new());
105 }
106 validate_env()?;
107 let analyzer = query::QueryAnalyzer::new();
108 let analyzed = analyzer.analyze(&statements[0].sql);
110 let key = connection_key().ok_or_else(|| {
111 DactylError::Adapter("no adapter configured: set DATASTORE and DATASTORE_ROUTE".into())
112 })?;
113 let adapter = connection(&key, &statements[0].sql, &analyzed)?;
114
115 let rewritten: Vec<Statement> = statements
117 .iter()
118 .map(|s| {
119 let a = analyzer.analyze(&s.sql);
120 Statement {
121 sql: a.rewrite.apply(&s.sql),
122 params: s.params.clone(),
123 }
124 })
125 .collect();
126
127 adapter.execute_batch(&rewritten)
128}
129
130fn validate_env() -> Result<(), DactylError> {
131 if let Ok(ds) = std::env::var("DATASTORE") {
132 if ds != "sqlite" && ds != "neon" {
133 return Err(DactylError::Adapter(
134 "invalid DATASTORE value: must be 'sqlite' or 'neon'".to_string(),
135 ));
136 }
137 if std::env::var("DATASTORE_ROUTE").is_err() {
138 return Err(DactylError::Adapter(
139 "DATASTORE_ROUTE is required when DATASTORE is set".into(),
140 ));
141 }
142 } else {
143 let has_legacy = std::env::var("DACTYL_NEON_ENDPOINT").is_ok()
144 || std::env::var("DACTYL_SQLITE_PATH").is_ok();
145 if !has_legacy {
146 return Err(DactylError::Adapter(
147 "no adapter configured: set DATASTORE and DATASTORE_ROUTE".into(),
148 ));
149 }
150 }
151 Ok(())
152}
153
154fn dispatch(
155 query: &str,
156 params: &[Parameter],
157 optimize: bool,
158 write: bool,
159) -> Result<Rows, DactylError> {
160 validate_env()?;
161
162 let analyzer = query::QueryAnalyzer::new();
163 let analyzed = analyzer.analyze(query);
164
165 let inferred_dialect = infer_dialect(&analyzed);
166
167 let key = connection_key().ok_or_else(|| {
168 DactylError::Adapter("no adapter configured: set DATASTORE and DATASTORE_ROUTE".into())
169 })?;
170 let adapter = connection(&key, query, &analyzed)?;
171 if !optimize {
172 if let Some(c) = query::first_unsupported(&analyzed.constructs, inferred_dialect) {
173 return Err(DactylError::Unsupported { construct: c });
174 }
175 }
176
177 let sql = analyzed.rewrite.apply(query);
178 adapter.execute(&sql, params, optimize, write)
179}
180
181fn infer_dialect(analyzed: &query::Analyzed) -> query::Dialect {
182 if let Some(override_ds) = analyzed.inline_override {
183 return match override_ds {
184 "neon" => query::Dialect::Postgres,
185 _ => query::Dialect::Sqlite,
186 };
187 }
188 if let Ok(ds) = std::env::var("DATASTORE") {
189 return match ds.as_str() {
190 "neon" => query::Dialect::Postgres,
191 _ => query::Dialect::Sqlite,
192 };
193 }
194 if std::env::var("DACTYL_NEON_ENDPOINT").is_ok() {
195 query::Dialect::Postgres
196 } else {
197 query::Dialect::Sqlite
198 }
199}
200
201fn connection(
202 key: &str,
203 query: &str,
204 analyzed: &query::Analyzed,
205) -> Result<Arc<dyn Adapter>, DactylError> {
206 {
207 let guard = connections()
208 .lock()
209 .expect("dactyl connection lock poisoned");
210 if let Some(existing) = guard.get(key) {
211 return Ok(existing.clone());
212 }
213 }
214 let adapter = build_adapter(query, analyzed)?;
215 let mut guard = connections()
216 .lock()
217 .expect("dactyl connection lock poisoned");
218 if let Some(existing) = guard.get(key) {
219 return Ok(existing.clone());
220 }
221 guard.insert(key.to_string(), adapter.clone());
222 Ok(adapter)
223}
224
225fn connection_key() -> Option<String> {
226 if let Ok(ds) = std::env::var("DATASTORE") {
227 if let Ok(route) = std::env::var("DATASTORE_ROUTE") {
228 return Some(format!("{ds}:{route}"));
229 }
230 }
231 if let Ok(endpoint) = std::env::var("DACTYL_NEON_ENDPOINT") {
232 return Some(format!("neon:{endpoint}"));
233 }
234 if let Ok(path) = std::env::var("DACTYL_SQLITE_PATH") {
235 return Some(format!("sqlite:{path}"));
236 }
237 None
238}
239
240#[cfg(feature = "sqlite")]
241fn sqlite_adapter(path: &str) -> Result<Arc<dyn Adapter>, DactylError> {
242 use crate::adapter::sqlite::SqliteAdapter;
243 let adapter =
244 SqliteAdapter::open(path).map_err(|e| DactylError::Adapter(format!("sqlite open: {e}")))?;
245 Ok(Arc::new(adapter))
246}
247
248#[cfg(feature = "neon")]
249fn neon_adapter() -> Result<Arc<dyn Adapter>, DactylError> {
250 use crate::adapter::neon::NeonAdapter;
251 let (endpoint, bearer) = resolve_neon_config()?;
252 let adapter = NeonAdapter::new(&endpoint, bearer, None);
253 Ok(Arc::new(adapter))
254}
255
256#[cfg(not(feature = "sqlite"))]
257fn sqlite_adapter(_path: &str) -> Result<Arc<dyn Adapter>, DactylError> {
258 Err(DactylError::Adapter(
259 "sqlite adapter requested but `sqlite` feature is disabled".into(),
260 ))
261}
262
263#[cfg(not(feature = "neon"))]
264fn neon_adapter() -> Result<Arc<dyn Adapter>, DactylError> {
265 Err(DactylError::Adapter(
266 "neon adapter requested but `neon` feature is disabled".into(),
267 ))
268}
269
270#[cfg(feature = "neon")]
271fn resolve_neon_config() -> Result<(String, Option<String>), DactylError> {
272 if let Ok(ds) = std::env::var("DATASTORE") {
273 if ds == "neon" {
274 let route = std::env::var("DATASTORE_ROUTE")
275 .map_err(|_| DactylError::Adapter("DATASTORE_ROUTE not set".into()))?;
276 let token = std::env::var("DATASTORE_TOKEN")
277 .ok()
278 .or_else(|| std::env::var("DACTYL_NEON_BEARER").ok());
279 return Ok((route, token));
280 }
281 }
282 let endpoint = std::env::var("DACTYL_NEON_ENDPOINT")
283 .map_err(|_| DactylError::Adapter("DACTYL_NEON_ENDPOINT not set".into()))?;
284 let bearer = std::env::var("DACTYL_NEON_BEARER").ok();
285 Ok((endpoint, bearer))
286}
287
288fn resolve_sqlite_path(query: &str) -> Result<String, DactylError> {
289 if let Ok(ds) = std::env::var("DATASTORE") {
290 if ds == "sqlite" {
291 let route = std::env::var("DATASTORE_ROUTE")
292 .map_err(|_| DactylError::Adapter("DATASTORE_ROUTE not set".into()))?;
293 return Ok(route);
294 }
295 }
296 if let Ok(p) = std::env::var("DACTYL_SQLITE_PATH") {
297 return Ok(p);
298 }
299 let default_root =
300 std::env::var("DACTYL_SQLITE_ROOT").unwrap_or_else(|_| ".decapod/data".to_string());
301 let store = infer_store(query).unwrap_or_else(|| "dactyl".to_string());
302 Ok(format!("{default_root}/{store}.db"))
303}
304
305fn build_adapter(query: &str, analyzed: &query::Analyzed) -> Result<Arc<dyn Adapter>, DactylError> {
306 let neon_env = if let Ok(ds) = std::env::var("DATASTORE") {
307 ds == "neon"
308 } else {
309 std::env::var("DACTYL_NEON_ENDPOINT").is_ok()
310 };
311 let sqlite_only = !analyzed.constructs.is_empty()
312 && analyzed
313 .constructs
314 .iter()
315 .all(|c| c.dialect() == query::Dialect::Sqlite);
316
317 if neon_env && !sqlite_only {
318 neon_adapter()
319 } else {
320 sqlite_adapter(&resolve_sqlite_path(query)?)
321 }
322}
323
324fn infer_store(query: &str) -> Option<String> {
325 let lower = query.to_ascii_lowercase();
326 let mut iter = lower.split_whitespace();
327 while let Some(tok) = iter.next() {
328 if tok == "from" || tok == "into" || tok == "update" || tok == "table" {
329 if let Some(name) = iter.next() {
330 return Some(name.trim_end_matches(';').to_string());
331 }
332 }
333 }
334 None
335}