1pub mod adapter;
27pub mod error;
28pub mod query;
29mod rows;
30
31#[doc(hidden)]
32pub mod __private;
33
34pub use dactyl_db_macros::query;
35
36pub use crate::error::DactylError;
37pub use crate::rows::{Row, Rows};
38
39use std::sync::{Arc, Mutex, OnceLock};
40
41use crate::adapter::Adapter;
42
43static CONNECTIONS: OnceLock<Mutex<std::collections::HashMap<String, Arc<dyn Adapter>>>> =
46 OnceLock::new();
47
48fn connections() -> &'static Mutex<std::collections::HashMap<String, Arc<dyn Adapter>>> {
49 CONNECTIONS.get_or_init(|| Mutex::new(std::collections::HashMap::new()))
50}
51
52#[doc(hidden)]
55pub fn __reset_for_tests() {
56 let mut guard = connections()
57 .lock()
58 .expect("dactyl connection lock poisoned");
59 guard.clear();
60}
61
62pub fn read(query: &str, optimize: bool) -> Result<Rows, DactylError> {
67 dispatch(query, optimize, false)
68}
69
70pub fn write(query: &str, optimize: bool) -> Result<Rows, DactylError> {
72 dispatch(query, optimize, true)
73}
74
75fn validate_env() -> Result<(), DactylError> {
76 if let Ok(ds) = std::env::var("DATASTORE") {
77 if ds != "sqlite" && ds != "neon" {
78 return Err(DactylError::Adapter(
79 "invalid DATASTORE value: must be 'sqlite' or 'neon'".to_string(),
80 ));
81 }
82 if std::env::var("DATASTORE_ROUTE").is_err() {
83 return Err(DactylError::Adapter(
84 "DATASTORE_ROUTE is required when DATASTORE is set".into(),
85 ));
86 }
87 } else {
88 let has_legacy = std::env::var("DACTYL_NEON_ENDPOINT").is_ok()
89 || std::env::var("DACTYL_SQLITE_PATH").is_ok();
90 if !has_legacy {
91 return Err(DactylError::Adapter(
92 "no adapter configured: set DATASTORE and DATASTORE_ROUTE".into(),
93 ));
94 }
95 }
96 Ok(())
97}
98
99fn dispatch(query: &str, optimize: bool, write: bool) -> Result<Rows, DactylError> {
100 validate_env()?;
101
102 let analyzer = query::QueryAnalyzer::new();
103 let analyzed = analyzer.analyze(query);
104
105 let inferred_dialect = infer_dialect(&analyzed);
109
110 let key = connection_key().ok_or_else(|| {
112 DactylError::Adapter("no adapter configured: set DATASTORE and DATASTORE_ROUTE".into())
113 })?;
114 let adapter = connection(&key, query, &analyzed)?;
115 if !optimize {
116 if let Some(c) = query::first_unsupported(&analyzed.constructs, inferred_dialect) {
117 return Err(DactylError::Unsupported { construct: c });
118 }
119 }
120
121 let sql = analyzed.rewrite.apply(query);
122 let params = serde_json::Value::Null;
123 adapter.execute(&sql, Some(¶ms), optimize, write)
124}
125
126fn infer_dialect(analyzed: &query::Analyzed) -> query::Dialect {
127 if let Some(override_ds) = analyzed.inline_override {
131 return match override_ds {
132 "neon" => query::Dialect::Postgres,
133 _ => query::Dialect::Sqlite,
134 };
135 }
136 if let Ok(ds) = std::env::var("DATASTORE") {
137 return match ds.as_str() {
138 "neon" => query::Dialect::Postgres,
139 _ => query::Dialect::Sqlite,
140 };
141 }
142 if std::env::var("DACTYL_NEON_ENDPOINT").is_ok() {
143 query::Dialect::Postgres
144 } else {
145 query::Dialect::Sqlite
146 }
147}
148
149fn connection(
151 key: &str,
152 query: &str,
153 analyzed: &query::Analyzed,
154) -> Result<Arc<dyn Adapter>, DactylError> {
155 {
156 let guard = connections()
157 .lock()
158 .expect("dactyl connection lock poisoned");
159 if let Some(existing) = guard.get(key) {
160 return Ok(existing.clone());
161 }
162 }
163 let adapter = build_adapter(query, analyzed)?;
164 let mut guard = connections()
165 .lock()
166 .expect("dactyl connection lock poisoned");
167 if let Some(existing) = guard.get(key) {
168 return Ok(existing.clone());
169 }
170 guard.insert(key.to_string(), adapter.clone());
171 Ok(adapter)
172}
173
174fn connection_key() -> Option<String> {
177 if let Ok(ds) = std::env::var("DATASTORE") {
178 if let Ok(route) = std::env::var("DATASTORE_ROUTE") {
179 return Some(format!("{ds}:{route}"));
180 }
181 }
182 if let Ok(endpoint) = std::env::var("DACTYL_NEON_ENDPOINT") {
183 return Some(format!("neon:{endpoint}"));
184 }
185 if let Ok(path) = std::env::var("DACTYL_SQLITE_PATH") {
186 return Some(format!("sqlite:{path}"));
187 }
188 None
189}
190
191#[cfg(feature = "sqlite")]
192fn sqlite_adapter(path: &str) -> Result<Arc<dyn Adapter>, DactylError> {
193 use crate::adapter::sqlite::SqliteAdapter;
194 let adapter =
195 SqliteAdapter::open(path).map_err(|e| DactylError::Adapter(format!("sqlite open: {e}")))?;
196 Ok(Arc::new(adapter))
197}
198
199#[cfg(feature = "neon")]
200fn neon_adapter() -> Result<Arc<dyn Adapter>, DactylError> {
201 use crate::adapter::neon::NeonAdapter;
202 let (endpoint, bearer) = resolve_neon_config()?;
203 let adapter = NeonAdapter::new(&endpoint, bearer, None);
204 Ok(Arc::new(adapter))
205}
206
207#[cfg(not(feature = "sqlite"))]
208fn sqlite_adapter(_path: &str) -> Result<Arc<dyn Adapter>, DactylError> {
209 Err(DactylError::Adapter(
210 "sqlite adapter requested but `sqlite` feature is disabled".into(),
211 ))
212}
213
214#[cfg(not(feature = "neon"))]
215fn neon_adapter() -> Result<Arc<dyn Adapter>, DactylError> {
216 Err(DactylError::Adapter(
217 "neon adapter requested but `neon` feature is disabled".into(),
218 ))
219}
220
221#[cfg(feature = "neon")]
222fn resolve_neon_config() -> Result<(String, Option<String>), DactylError> {
223 if let Ok(ds) = std::env::var("DATASTORE") {
224 if ds == "neon" {
225 let route = std::env::var("DATASTORE_ROUTE")
226 .map_err(|_| DactylError::Adapter("DATASTORE_ROUTE not set".into()))?;
227 let token = std::env::var("DATASTORE_TOKEN")
228 .ok()
229 .or_else(|| std::env::var("DACTYL_NEON_BEARER").ok());
230 return Ok((route, token));
231 }
232 }
233 let endpoint = std::env::var("DACTYL_NEON_ENDPOINT")
234 .map_err(|_| DactylError::Adapter("DACTYL_NEON_ENDPOINT not set".into()))?;
235 let bearer = std::env::var("DACTYL_NEON_BEARER").ok();
236 Ok((endpoint, bearer))
237}
238
239fn resolve_sqlite_path(query: &str) -> Result<String, DactylError> {
240 if let Ok(ds) = std::env::var("DATASTORE") {
241 if ds == "sqlite" {
242 let route = std::env::var("DATASTORE_ROUTE")
243 .map_err(|_| DactylError::Adapter("DATASTORE_ROUTE not set".into()))?;
244 return Ok(route);
245 }
246 }
247 if let Ok(p) = std::env::var("DACTYL_SQLITE_PATH") {
248 return Ok(p);
249 }
250 let default_root =
251 std::env::var("DACTYL_SQLITE_ROOT").unwrap_or_else(|_| ".decapod/data".to_string());
252 let store = infer_store(query).unwrap_or_else(|| "dactyl".to_string());
253 Ok(format!("{default_root}/{store}.db"))
254}
255
256fn build_adapter(query: &str, analyzed: &query::Analyzed) -> Result<Arc<dyn Adapter>, DactylError> {
257 let neon_env = if let Ok(ds) = std::env::var("DATASTORE") {
258 ds == "neon"
259 } else {
260 std::env::var("DACTYL_NEON_ENDPOINT").is_ok()
261 };
262 let sqlite_only = !analyzed.constructs.is_empty()
263 && analyzed
264 .constructs
265 .iter()
266 .all(|c| c.dialect() == query::Dialect::Sqlite);
267 let postgres_only = !analyzed.constructs.is_empty()
268 && analyzed
269 .constructs
270 .iter()
271 .all(|c| c.dialect() == query::Dialect::Postgres);
272
273 if neon_env && !sqlite_only {
274 neon_adapter()
275 } else if !neon_env && postgres_only {
276 sqlite_adapter(&resolve_sqlite_path(query)?)
280 } else {
281 sqlite_adapter(&resolve_sqlite_path(query)?)
282 }
283}
284
285fn infer_store(query: &str) -> Option<String> {
289 let lower = query.to_ascii_lowercase();
290 let mut iter = lower.split_whitespace();
291 while let Some(tok) = iter.next() {
292 if tok == "from" || tok == "into" || tok == "update" || tok == "table" {
293 if let Some(name) = iter.next() {
294 return Some(name.trim_end_matches(';').to_string());
295 }
296 }
297 }
298 None
299}