1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
//! Dactyl — the governed datastore boundary for Decapod.
//!
//! Interchangeably read and write to local SQLite or cloud-hosted Vercel Neon
//! instances behind a single unified facade.
//!
//! # Configuration
//!
//! The active datastore is selected by ambient environment variables — no
//! `init()` call and no per-call datastore argument:
//!
//! | Variable | Required | Meaning |
//! |--------------------|----------|-----------------------------------------------------------------|
//! | `DATASTORE` | yes | `"sqlite"` or `"neon"`. Any other value is a typed error. |
//! | `DATASTORE_ROUTE` | yes | SQLite file path (sqlite) or Neon/Propodus endpoint URL (neon). |
//! | `DATASTORE_TOKEN` | no | Opaque bearer token forwarded to the Neon adapter. Ignored by sqlite. |
//!
//! Each call opens its own short-lived adapter execution and drops it on
//! return — there is no process-wide connection cache, so workspace and
//! session isolation is automatic and the public surface is `Send + Sync`
//! without any lock.
//!
//! # Public surface
//!
//! - [`query`] — one entry point for any SQL (read or write).
//! - [`execute`] — DDL / migration / affected-row operations.
//! - [`transaction`] — atomic batch of parameterized statements.
//! - [`query!`] — compile-time SQL literal analyzer.
//!
//! No `init`, no `read`/`write` split, no `optimize` flag. The query analyzer
//! is still available via [`query::QueryAnalyzer`] for callers that want
//! compile-time dialect visibility; runtime dispatch always passes the SQL
//! straight to the active adapter.
pub use query;
pub use crateDactylError;
pub use crate;
use crateAdapter;
/// A parameterized SQL statement for batch execution.
/// Test-only helper retained for the conformance harness. With no process-wide
/// cache there is nothing to clear; the function is a no-op kept only so
/// existing tests do not have to be rewritten around its removal.
/// Resolve the active datastore triple from ambient env vars.
///
/// Returns a typed error if `DATASTORE` is missing or unrecognized, or if
/// `DATASTORE_ROUTE` is missing.
/// Execute any SQL statement against the active datastore and return its
/// rows. Reads return the matched rows; writes return any returned rows
/// (Neon/`RETURNING`) or an empty [`Rows`] when the adapter surfaces no rows.
///
/// Parameters are bound by the adapter — never interpolated into the SQL.
/// The statement is executed against the datastore selected by the ambient
/// `DATASTORE` / `DATASTORE_ROUTE` / optional `DATASTORE_TOKEN` env vars.
/// Execute a DDL / migration / affected-row operation against the active
/// datastore and return the number of affected rows.
///
/// This is the caller-owned schema surface (dactyl #27): dactyl never
/// silently creates tables — callers own and version their schema through
/// explicit `execute` calls. Schemas opened against a caller-owned database
/// are not mutated beyond the statements the caller issues.
/// Execute an atomic batch of parameterized statements.
///
/// # Atomicity (dactyl #24)
///
/// On any per-statement error the whole unit rolls back and the function
/// returns [`DactylError`]; **no partial state is committed**. Semantics:
///
/// | Backend | Mechanism |
/// |---|---|
/// | SQLite | Single rusqlite transaction: begin → statements → commit, or drop = rollback |
/// | Neon | One `POST {endpoint}/batch` request; the server must accept/reject the batch as a unit |
///
/// An empty `statements` slice is a successful no-op and returns `Ok(vec![])`.
///
/// # Nesting
///
/// **Not supported.** Each call builds a fresh short-lived adapter. There is no
/// SAVEPOINT API and no nesting of `transaction` inside another open unit.
/// Independent concurrent `transaction` calls are separate atomic units, not
/// nested subtransactions. Callers that need multi-step atomicity must put
/// every statement in a **single** `transaction(&[...])` slice.
///
/// # Retry
///
/// **dactyl does not retry.** A failed batch leaves no committed partial state
/// on either adapter (when the Neon transport returns a definitive error).
/// Callers own retry policy. After a **transport timeout or dropped connection**,
/// the client cannot distinguish “never applied” from “applied but response
/// lost”; retries must use **idempotent** statement design (deterministic keys,
/// upserts) if re-execution is possible.
///
/// # Timeout
///
/// **No public deadline parameter.** SQLite is process-local. Neon uses the
/// reqwest client’s default timeouts. Callers that need tighter bounds should
/// enforce them outside dactyl (process supervisor, HTTP proxy, or a future
/// env-based client config — not part of this surface).
///
/// # Idempotency
///
/// `transaction` itself is **not** idempotent. Replaying a previously
/// successful batch may insert duplicates or hit primary-key conflicts.
/// Design statements for safe replay when the caller’s retry policy may
/// re-submit after ambiguous failures.
///
/// # Returns
///
/// On success, one [`Rows`] per input statement (writes often yield empty
/// row sets; `SELECT` statements yield projections). On failure, an
/// [`DactylError::Adapter`] (or conversion error while decoding Neon rows)
/// and no committed partial state.
/// Construct a fresh, short-lived adapter for one call. No caching, no
/// shared mutable state — the returned adapter is dropped at the end of the
/// caller's call. Thread safety follows from this: nothing is shared between
/// calls, so there is no lock acquisition order to define.