Skip to main content

nedb_engine/
server.rs

1// SPDX-FileCopyrightText: 2026 INTERCHAINED LLC
2// SPDX-License-Identifier: BUSL-1.1
3// NEDB · © 2026 INTERCHAINED LLC × Eth-Interchained × Vex (Claude Opus 5)
4
5//! nedbd v2 HTTP server — same /v1/databases/* API surface as v1.
6//! Drop-in replacement: Vision, itsl_mirror, all existing clients work unchanged.
7//!
8//! Built on tokio + axum. Each database is opened once and held in an Arc<RwLock>.
9//! All write paths use the Db's internal atomic operations; the RwLock is only
10//! needed to protect the manager's HashMap (open/close operations), not individual
11//! document writes (which are lock-free at the content-addressed level).
12
13use std::collections::HashMap;
14use std::path::{Path, PathBuf};
15use std::sync::Arc;
16use std::sync::atomic::AtomicU64;
17
18use axum::{
19    extract::{Path as AxPath, State, Query as AxQuery},
20    http::{HeaderMap, StatusCode},
21    response::{IntoResponse, Response, sse::{Event, KeepAlive, Sse}},
22    routing::{delete, get, post},
23    Json, Router,
24};
25use dashmap::DashMap;
26use serde::Deserialize;
27use serde_json::{json, Value};
28use tokio::sync::{broadcast, RwLock};
29use tokio_stream::wrappers::BroadcastStream;
30use tokio_stream::StreamExt as _;
31
32use crate::db::Db;
33use crate::nql;
34use crate::store::Node;
35
36// ── Log channel — broadcast to all /events SSE subscribers ────────────────────
37
38const LOG_CHANNEL_CAP: usize = 512;
39const SUB_CHANNEL_CAP: usize = 256;
40
41// ── Subscription registry ─────────────────────────────────────────────────────
42// Maps (db_name, sub_id) → (nql_query, result_hash, event_sender)
43// After every write, all registered queries for that db are re-evaluated.
44// Diffs (added/removed/changed rows) are emitted as SSE events.
45
46type SubKey = (String, u64);  // (db_name, sub_id)
47type SubVal = (String, String, broadcast::Sender<String>);  // (nql, last_hash, tx)
48
49/// Send a timestamped log line to both stdout and all /events subscribers.
50macro_rules! nlog {
51    ($tx:expr, $($arg:tt)*) => {{
52        let line = format!($($arg)*);
53        println!("{}", line);
54        let _ = $tx.send(line);
55    }};
56}
57
58// ── Manager ───────────────────────────────────────────────────────────────────
59
60#[derive(Clone)]
61pub struct Manager {
62    inner:     Arc<RwLock<ManagerInner>>,
63    pub token: Option<String>,
64    /// Broadcast channel — every log line goes here; /events streams them.
65    pub log_tx: broadcast::Sender<String>,
66    /// Live query subscriptions: (db_name, sub_id) → (nql, last_hash, event_tx)
67    subs:    Arc<DashMap<SubKey, SubVal>>,
68    sub_ctr: Arc<AtomicU64>,
69    /// Natural-language planner. None unless built with --features cast AND
70    /// enabled at runtime; the whole feature is opt-in so a default nedbd
71    /// carries no model and no extra bytes.
72    #[cfg(feature = "cast")]
73    pub caster: Option<crate::cast::Caster>,
74}
75
76struct ManagerInner {
77    data_dir:    PathBuf,
78    dbs:         HashMap<String, Arc<Db>>,
79    tmk:         Option<[u8; 32]>,
80    memory_mode: bool,
81}
82
83impl Manager {
84    pub fn new(data_dir: &Path, tmk: Option<[u8; 32]>, token: Option<String>, memory_mode: bool) -> Self {
85        let (log_tx, _) = broadcast::channel(LOG_CHANNEL_CAP);
86        Self {
87            inner: Arc::new(RwLock::new(ManagerInner {
88                data_dir: data_dir.to_path_buf(),
89                dbs:      HashMap::new(),
90                tmk,
91                memory_mode,
92            })),
93            token,
94            log_tx,
95            #[cfg(feature = "cast")]
96            caster: None,
97            subs:    Arc::new(DashMap::new()),
98            sub_ctr: Arc::new(AtomicU64::new(1)),
99        }
100    }
101
102    /// Register a live query subscription. Returns (sub_id, receiver).
103    fn subscribe(&self, db: &str, nql: String) -> (u64, broadcast::Receiver<String>) {
104        use std::sync::atomic::Ordering;
105        let id = self.sub_ctr.fetch_add(1, Ordering::Relaxed);
106        let (tx, rx) = broadcast::channel(SUB_CHANNEL_CAP);
107        self.subs.insert((db.to_string(), id), (nql, String::new(), tx));
108        (id, rx)
109    }
110
111    /// Unregister a subscription.
112    fn unsubscribe(&self, db: &str, sub_id: u64) {
113        self.subs.remove(&(db.to_string(), sub_id));
114    }
115
116    /// After a write: re-evaluate all subscriptions for `db`, emit diffs.
117    fn notify_subscribers(&self, db: &str, db_arc: &Arc<crate::db::Db>) {
118        let keys: Vec<SubKey> = self.subs.iter()
119            .filter(|e| e.key().0 == db)
120            .map(|e| e.key().clone())
121            .collect();
122
123        for key in keys {
124            if let Some(mut entry) = self.subs.get_mut(&key) {
125                let (nql, last_hash, tx) = entry.value_mut();
126                // Re-run the query
127                let rows = match crate::nql::query(db_arc, nql) {
128                    Ok((rows, _)) => rows,
129                    Err(_) => continue,
130                };
131                // Hash the result set
132                let new_hash = format!("{:?}", rows.iter().map(|r| r.to_string()).collect::<Vec<_>>());
133                if new_hash == *last_hash { continue; }
134                *last_hash = new_hash;
135                // Send the full current result as a diff event
136                let event = json!({
137                    "sub_id": key.1,
138                    "db":     &key.0,
139                    "nql":    nql.as_str(),
140                    "rows":   rows,
141                    "count":  rows.len(),
142                });
143                let _ = tx.send(event.to_string());
144            }
145        }
146    }
147
148    /// Open all existing databases in the data directory on startup.
149    pub async fn open_all(&self) -> anyhow::Result<()> {
150        let (data_dir, tmk, memory_mode) = {
151            let inner = self.inner.read().await;
152            (inner.data_dir.clone(), inner.tmk, inner.memory_mode)
153        };
154        // In memory mode: nothing to open from disk — all DBs created on first write
155        if memory_mode { return Ok(()); }
156        if !data_dir.exists() {
157            std::fs::create_dir_all(&data_dir)?;
158            return Ok(());
159        }
160        let mut names = vec![];
161        for entry in std::fs::read_dir(&data_dir)? {
162            let entry = entry?;
163            if entry.file_type()?.is_dir() {
164                names.push(entry.file_name().to_string_lossy().to_string());
165            }
166        }
167        let log_tx = self.log_tx.clone();
168        let mut inner = self.inner.write().await;
169        for name in names {
170            let db_path = inner.data_dir.join(&name);
171            let dek = tmk.map(|k| crate::store::Dek::from_tmk(&k, name.as_bytes()));
172            match Db::open(&db_path, dek) {
173                Ok(db) => {
174                    nlog!(log_tx, "  [nedbd] opened database {:?}", name);
175                    let db_arc = Arc::new(db);
176                    Db::start_cold_scan(Arc::clone(&db_arc));
177                    // Flush MANIFEST every 1s in background — removes I/O from write path
178                    Db::start_manifest_ticker(Arc::clone(&db_arc), 1000);
179                    inner.dbs.insert(name, db_arc);
180                }
181                Err(e) => nlog!(log_tx, "  [nedbd] ERROR opening {:?}: {}", name, e),
182            }
183        }
184        Ok(())
185    }
186
187    async fn get_db(&self, name: &str) -> Option<Arc<Db>> {
188        self.inner.read().await.dbs.get(name).cloned()
189    }
190
191    async fn create_db(&self, name: &str) -> anyhow::Result<Arc<Db>> {
192        let (data_dir, tmk, memory_mode) = {
193            let inner = self.inner.read().await;
194            (inner.data_dir.clone(), inner.tmk, inner.memory_mode)
195        };
196        let db = if memory_mode {
197            // Pure in-memory — instant, no files
198            Arc::new(Db::in_memory())
199        } else {
200            let db_path = data_dir.join(name);
201            let dek = tmk.map(|k| crate::store::Dek::from_tmk(&k, name.as_bytes()));
202            let db = Arc::new(Db::open(&db_path, dek)?);
203            Db::start_cold_scan(Arc::clone(&db));
204            Db::start_manifest_ticker(Arc::clone(&db), 1000);
205            db
206        };
207        self.inner.write().await.dbs.insert(name.to_string(), db.clone());
208        Ok(db)
209    }
210
211    async fn drop_db(&self, name: &str) -> bool {
212        let db = self.inner.write().await.dbs.remove(name);
213        if let Some(db) = db {
214            // Flush manifest before dropping
215            db.flush_manifest_if_dirty();
216            let data_dir = self.inner.read().await.data_dir.clone();
217            let _ = std::fs::remove_dir_all(data_dir.join(name));
218            true
219        } else {
220            false
221        }
222    }
223
224    /// Flush all open databases (id-index WAL + MANIFEST) — call on graceful shutdown.
225    pub async fn flush_all(&self) {
226        let inner = self.inner.read().await;
227        for db in inner.dbs.values() {
228            db.flush_all();  // WAL + manifest
229        }
230    }
231
232    async fn names(&self) -> Vec<String> {
233        self.inner.read().await.dbs.keys().cloned().collect()
234    }
235
236    /// Emit a log line to stdout and all /events SSE subscribers.
237    pub fn log(&self, msg: impl Into<String>) {
238        let line = msg.into();
239        println!("{}", line);
240        let _ = self.log_tx.send(line);
241    }
242
243    fn check_auth(&self, headers: &HeaderMap) -> bool {
244        match &self.token {
245            None => true,
246            Some(required) => {
247                if let Some(auth) = headers.get("authorization") {
248                    if let Ok(s) = auth.to_str() {
249                        return s == format!("Bearer {}", required);
250                    }
251                }
252                false
253            }
254        }
255    }
256}
257
258// ── Error helpers ─────────────────────────────────────────────────────────────
259
260fn err(status: StatusCode, msg: &str) -> Response {
261    (status, Json(json!({"error": msg}))).into_response()
262}
263
264fn ok(body: Value) -> Response {
265    (StatusCode::OK, Json(body)).into_response()
266}
267
268/// Return (seq, head) — both O(1) reads from in-memory atomics/cache.
269/// The head is maintained incrementally by Db::put() and Db::delete()
270/// so we never recompute it from scratch on every response.
271fn db_seq_head(db: &Db) -> (u64, String) {
272    let seq  = db.seq.load(std::sync::atomic::Ordering::SeqCst);
273    let head = db.head();
274    (seq, head)
275}
276
277// ── Route handlers ────────────────────────────────────────────────────────────
278
279async fn health(State(mgr): State<Manager>) -> Response {
280    let names = mgr.names().await;
281    let inner = mgr.inner.read().await;
282    ok(json!({
283        "ok":        true,
284        "service":   "nedbd",
285        "version":   env!("CARGO_PKG_VERSION"),
286        "engine":    "dag",
287        "memory":    inner.memory_mode,
288        "databases": names,
289        "encrypted": inner.tmk.is_some(),
290    }))
291}
292
293async fn list_databases(State(mgr): State<Manager>, headers: HeaderMap) -> Response {
294    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
295    let names = mgr.names().await;
296    let summaries: Vec<Value> = {
297        let inner = mgr.inner.read().await;
298        names.iter().map(|n| {
299            if let Some(db) = inner.dbs.get(n) {
300                let (seq, head) = db_seq_head(db);
301                json!({"name": n, "seq": seq, "head": head, "collections": db.collections()})
302            } else {
303                json!({"name": n})
304            }
305        }).collect()
306    };
307    ok(json!({"databases": summaries}))
308}
309
310#[derive(Deserialize)]
311struct CreateDbBody { name: String }
312
313async fn create_database(
314    State(mgr): State<Manager>,
315    headers: HeaderMap,
316    Json(body): Json<CreateDbBody>,
317) -> Response {
318    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
319    if body.name.is_empty() { return err(StatusCode::BAD_REQUEST, "name is required"); }
320    match mgr.create_db(&body.name).await {
321        Ok(db) => {
322            let (seq, head) = db_seq_head(&db);
323            (StatusCode::CREATED, Json(json!({"database": {"name": body.name, "seq": seq, "head": head}}))).into_response()
324        }
325        Err(e) => err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
326    }
327}
328
329async fn get_database(
330    State(mgr): State<Manager>,
331    headers: HeaderMap,
332    AxPath(name): AxPath<String>,
333) -> Response {
334    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
335    match mgr.get_db(&name).await {
336        None => err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
337        Some(db) => {
338            let (seq, head) = db_seq_head(&db);
339            ok(json!({"name": name, "seq": seq, "head": head, "collections": db.collections()}))
340        }
341    }
342}
343
344async fn drop_database(
345    State(mgr): State<Manager>,
346    headers: HeaderMap,
347    AxPath(name): AxPath<String>,
348) -> Response {
349    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
350    let dropped = mgr.drop_db(&name).await;
351    ok(json!({"dropped": dropped}))
352}
353
354#[derive(Deserialize)]
355struct QueryBody { nql: String }
356
357// ── Natural-language planning (feature: cast) ─────────────────────────────────
358
359// Both fields are read only by the `cast`-enabled handler. The
360// `cfg(not(feature = "cast"))` stub still deserializes this body — so that a
361// malformed request is rejected as 400 before the 501, keeping the two builds
362// behaviourally consistent — but never looks at the values, which without this
363// attribute produces a dead_code warning on every default build.
364#[cfg_attr(not(feature = "cast"), allow(dead_code))]
365#[derive(Deserialize)]
366struct CastBody {
367    prompt: String,
368    /// Run the plan immediately. Defaults to FALSE on purpose: the endpoint hands
369    /// back a plan for review rather than executing a guess. A planner that
370    /// silently runs the wrong query is worse than one that admits uncertainty.
371    #[serde(default)]
372    execute: bool,
373}
374
375/// POST /v1/databases/:name/cast — turn a short English prompt into NQL.
376///
377/// The model only ever produces TEXT. Execution goes through the same
378/// `nql::query` path a hand-typed query uses, so there is no second code path
379/// with different validation.
380#[cfg(feature = "cast")]
381async fn cast_prompt(
382    State(mgr): State<Manager>,
383    headers: HeaderMap,
384    AxPath(name): AxPath<String>,
385    Json(body): Json<CastBody>,
386) -> Response {
387    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
388
389    let caster = match &mgr.caster {
390        Some(c) => c,
391        None => return err(
392            StatusCode::SERVICE_UNAVAILABLE,
393            "cast is not enabled; start nedbd with --cast (or NEDBD_CAST=1) \
394             and place model.cast in the data directory",
395        ),
396    };
397
398    let db = match mgr.get_db(&name).await {
399        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
400        Some(db) => db,
401    };
402    if body.prompt.trim().is_empty() {
403        return err(StatusCode::BAD_REQUEST, "prompt is required");
404    }
405
406    // The engine knows the real schema, so constrain against it. This is the
407    // whole reason the planner lives here instead of in a client.
408    let collections = db.collections();
409    let result = caster.cast_checked(&body.prompt, &collections);
410
411    // Validate by PARSING, not by pattern-matching the text. The parser is the
412    // only authority on whether something is runnable.
413    let parse_err = match nql::parse(&result.nql) {
414        Ok(_)  => None,
415        Err(e) => Some(e.to_string()),
416    };
417
418    let (seq, head) = db_seq_head(&db);
419    let mut out = json!({
420        "prompt":            body.prompt,
421        "nql":               result.nql,
422        "valid":             parse_err.is_none(),
423        "collection":        result.collection,
424        "collection_known":  result.collection_known,
425        "collections":       collections,
426        "executed":          false,
427        "seq":  seq,
428        "head": head,
429    });
430
431    // A literal the model invented rather than copied. Advisory, not fatal —
432    // the plan is well-formed and may be exactly right, so we surface it and
433    // let the caller judge. Warned-about-and-correct is a cost worth paying to
434    // avoid confidently-wrong-and-silent, which for an agent poisons every
435    // subsequent step. Absent from the response when there is nothing to say,
436    // so `"drift" in response` is a usable test.
437    if let Some(d) = &result.drift {
438        out["drift"] = json!(d);
439    }
440
441    if let Some(e) = parse_err {
442        // Report the failure WITH the offending text. Never swallow it into an
443        // empty result set — that reads as "no matching rows", which is a lie.
444        out["error"] = json!(format!("NQL error: {}", e));
445        return (StatusCode::UNPROCESSABLE_ENTITY, Json(out)).into_response();
446    }
447
448    if !result.collection_known {
449        // Parses fine, but names a collection this database does not have. That
450        // is a model miss, not a user error, and it deserves to be said plainly
451        // rather than returning zero rows.
452        out["error"] = json!(format!(
453            "collection {:?} does not exist in {:?}",
454            result.collection.unwrap_or_default(), name
455        ));
456        return (StatusCode::UNPROCESSABLE_ENTITY, Json(out)).into_response();
457    }
458
459    if !body.execute {
460        return ok(out);
461    }
462
463    // Same executor as /query. No special path.
464    let nql_text = out["nql"].as_str().unwrap_or("").to_string();
465    match nql::query(&db, &nql_text) {
466        Ok((rows, count)) => {
467            out["executed"] = json!(true);
468            out["rows"]     = json!(rows);
469            out["count"]    = json!(count);
470            ok(out)
471        }
472        Err(e) => {
473            out["error"] = json!(format!("NQL error: {}", e));
474            (StatusCode::BAD_REQUEST, Json(out)).into_response()
475        }
476    }
477}
478
479/// Stub so the route table compiles identically with the feature off. Callers get
480/// a clear 501 instead of a 404, which would wrongly suggest the URL is wrong.
481#[cfg(not(feature = "cast"))]
482async fn cast_prompt(
483    State(_mgr): State<Manager>,
484    _headers: HeaderMap,
485    AxPath(_name): AxPath<String>,
486    Json(_body): Json<CastBody>,
487) -> Response {
488    err(
489        StatusCode::NOT_IMPLEMENTED,
490        "this nedbd was built without the `cast` feature; \
491         rebuild with --features cast to enable natural-language planning",
492    )
493}
494
495async fn query_database(
496    State(mgr): State<Manager>,
497    headers: HeaderMap,
498    AxPath(name): AxPath<String>,
499    Json(body): Json<QueryBody>,
500) -> Response {
501    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
502    let db = match mgr.get_db(&name).await {
503        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
504        Some(db) => db,
505    };
506    if body.nql.trim().is_empty() {
507        return err(StatusCode::BAD_REQUEST, "a statement is required");
508    }
509    // THE FIELD IS STILL CALLED `nql`; ITS CONTENTS NO LONGER HAVE TO BE.
510    //
511    // This endpoint accepts neQL — NQL or PostgreSQL SQL — and routes on the
512    // leading keyword, using the same `neql::route` the CLI uses. The field
513    // name is kept because every existing HTTP client sends it, and renaming
514    // it would break them to gain nothing; what changed is what it accepts.
515    //
516    // A SQL statement sent here used to reach `nql::parse`, which reported
517    // something like `expected keyword FROM, got Ident("SELECT")` — an error
518    // about the wrong language, which reads as "NEDB does not understand
519    // SQL" when the truth was "this endpoint did not".
520    let dialect = match crate::neql::route(&body.nql) {
521        Ok(d) => d,
522        Err(why) => return err(StatusCode::BAD_REQUEST, &why),
523    };
524    let (seq, head) = db_seq_head(&db);
525    match dialect {
526        crate::neql::Dialect::Nql => match nql::query(&db, &body.nql) {
527            Ok((rows, count)) => ok(json!({
528                "rows": rows, "count": count, "seq": seq, "head": head,
529                "dialect": "nql",
530            })),
531            Err(e) => err(StatusCode::BAD_REQUEST, &format!("NQL error: {}", e)),
532        },
533        crate::neql::Dialect::Sql => match crate::pgwire::execute_sql(&db, &body.nql, false) {
534            Ok(done) => {
535                let n = done.rows.len();
536                ok(json!({
537                    "rows": done.rows, "count": n, "seq": seq, "head": head,
538                    "dialect": "sql", "tag": done.tag,
539                }))
540            }
541            Err(why) => err(StatusCode::BAD_REQUEST, &format!("SQL error: {}", why)),
542        },
543    }
544}
545
546#[derive(Deserialize)]
547struct PutBody {
548    coll:       String,
549    id:         String,
550    doc:        Value,
551    caused_by:  Option<Vec<serde_json::Value>>,
552    valid_from: Option<String>,
553    valid_to:   Option<String>,
554    #[allow(dead_code)] evidence:   Option<String>,
555    #[allow(dead_code)] confidence: Option<f64>,
556    #[allow(dead_code)] client:     Option<String>,
557    #[allow(dead_code)] nonce:      Option<u64>,
558    #[allow(dead_code)] idem:       Option<String>,
559}
560
561#[derive(Deserialize)]
562struct LinkBody {
563    frm: String,
564    rel: String,
565    to:  String,
566}
567
568async fn put_document(
569    State(mgr): State<Manager>,
570    headers: HeaderMap,
571    AxPath(name): AxPath<String>,
572    Json(body): Json<PutBody>,
573) -> Response {
574    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
575    let db = match mgr.get_db(&name).await {
576        None => {
577            // Auto-create database on first write
578            match mgr.create_db(&name).await {
579                Ok(db) => db,
580                Err(e) => return err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
581            }
582        }
583        Some(db) => db,
584    };
585    // Block writes until background startup scan completes (cold start only).
586    // Reads and queries always proceed immediately.
587    if !db.startup_ready.load(std::sync::atomic::Ordering::SeqCst) {
588        return err(StatusCode::SERVICE_UNAVAILABLE,
589            "database startup in progress — reads available, writes retry in a moment");
590    }
591    // Resolve caused_by items: accept hash strings (v2 native) OR seq integers (v1 compat).
592    let caused_by: Vec<String> = body.caused_by.unwrap_or_default()
593        .into_iter()
594        .filter_map(|v| match v {
595            serde_json::Value::String(s) => Some(s),
596            serde_json::Value::Number(n) => {
597                n.as_u64().and_then(|seq| db.get_hash_by_seq(seq))
598            }
599            _ => None,
600        })
601        .collect();
602    // Run synchronous file I/O (objects.write) on a blocking thread so concurrent
603    // PUTs don't serialize on the tokio async thread pool.
604    let coll = body.coll.clone();
605    let id   = body.id.clone();
606    let doc  = body.doc.clone();
607    let vf   = body.valid_from.clone();
608    let vt   = body.valid_to.clone();
609    let db2  = Arc::clone(&db);
610    let result = tokio::task::spawn_blocking(move || {
611        db2.put(&coll, &id, doc, caused_by, vf, vt)
612    }).await;
613    match result {
614        Err(join_err) => err(StatusCode::INTERNAL_SERVER_ERROR, &join_err.to_string()),
615        Ok(Err(e))    => err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
616        Ok(Ok(node))  => {
617            let (seq, head) = db_seq_head(&db);
618            mgr.notify_subscribers(&name, &db);
619            ok(json!({"ok": true, "doc": node_to_response(&node), "seq": seq, "head": head}))
620        }
621    }
622}
623
624fn node_to_response(node: &Node) -> Value {
625    json!({
626        "_id":   node.id,
627        "_hash": node.hash,
628        "_seq":  node.seq,
629        "_coll": node.coll,
630        "data":  node.data,
631    })
632}
633
634async fn link_document(
635    State(mgr): State<Manager>,
636    headers: HeaderMap,
637    AxPath(name): AxPath<String>,
638    Json(body): Json<LinkBody>,
639) -> Response {
640    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
641    let db = match mgr.get_db(&name).await {
642        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
643        Some(db) => db,
644    };
645    if !db.startup_ready.load(std::sync::atomic::Ordering::SeqCst) {
646        return err(StatusCode::SERVICE_UNAVAILABLE, "startup scan in progress");
647    }
648    match db.link(&body.frm, &body.rel, &body.to) {
649        Ok(()) => {
650            let (seq, head) = db_seq_head(&db);
651            ok(json!({"ok": true, "frm": body.frm, "rel": body.rel, "to": body.to, "seq": seq, "head": head}))
652        }
653        Err(e) => err(StatusCode::BAD_REQUEST, &e.to_string()),
654    }
655}
656
657/// `GET /v1/databases/:name/rows/:coll/:id` — fetch one document by id.
658///
659/// This route existed only for DELETE, so a client could remove a row by id
660/// over HTTP but not READ one: it had to build `FROM coll WHERE _id = "..."`
661/// and interpolate the id into a NQL string. That made every id containing a
662/// double quote unreachable — `client.get()` returned None, meaning "no such
663/// document", for a document `put()` had stored and `FROM coll` returned — and
664/// an id ending in a backslash could not be escaped at all, because the lexer
665/// collapses `\"` and would swallow the closing quote.
666///
667/// Taking the id from the URL path removes the string-building entirely: the
668/// id arrives percent-decoded and byte-exact, with no quoting to get wrong and
669/// no injection surface.
670///
671/// Returns the same flat row shape a query returns (`nql::node_to_json`), so
672/// callers that previously used `rows[0]` from a query see no change.
673///
674/// `?as_of=N` resolves the version at or before sequence N, which is the
675/// single-document form of time travel and previously had no HTTP surface at
676/// all.
677///
678/// A missing row is `200 {"row": null}` rather than 404 — see the note in the
679/// body for why that ambiguity had to go.
680async fn get_document(
681    State(mgr): State<Manager>,
682    headers: HeaderMap,
683    AxPath((name, coll, id)): AxPath<(String, String, String)>,
684    AxQuery(q): AxQuery<GetRowQuery>,
685) -> Response {
686    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
687    let db = match mgr.get_db(&name).await {
688        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
689        Some(db) => db,
690    };
691    let node = match q.as_of {
692        Some(seq) => db.get_as_of(&coll, &id, seq),
693        None      => db.get(&coll, &id),
694    };
695    // A MISSING ROW IS 200 WITH `row: null`, NOT 404.
696    //
697    // Deliberate, and it costs a little REST idiom to buy an unambiguous
698    // client. A client must work against two server implementations (this one
699    // and the Python AOF server) across several versions, and a server that
700    // does not have this route at all also answers 404 — so a 404 here would
701    // be indistinguishable from "route unavailable" and the client could not
702    // tell "the row is absent" from "fall back to the query path". With this
703    // shape: 200 means the route answered (row present or null), and any
704    // 404/405 means the route is not there.
705    let (seq, head) = db_seq_head(&db);
706    let row = match node {
707        None => Value::Null,
708        Some(n) => crate::nql::node_to_json(&n),
709    };
710    ok(json!({"row": row, "seq": seq, "head": head}))
711}
712
713#[derive(Deserialize, Default)]
714struct GetRowQuery {
715    as_of: Option<u64>,
716}
717
718async fn delete_document(
719    State(mgr): State<Manager>,
720    headers: HeaderMap,
721    AxPath((name, coll, id)): AxPath<(String, String, String)>,
722) -> Response {
723    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
724    let db = match mgr.get_db(&name).await {
725        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
726        Some(db) => db,
727    };
728    // v2 DAG: tombstone write + id index removal — doc history is preserved in the DAG,
729    // but the live id pointer is cleared so queries and list() never return the doc.
730    let existed = match db.delete(&coll, &id) {
731        Ok(v)  => v,
732        Err(e) => return err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
733    };
734    let (seq, head) = db_seq_head(&db);
735    ok(json!({"ok": existed, "seq": seq, "head": head}))
736}
737
738#[derive(Deserialize)]
739struct BatchOp {
740    op:  String,
741    coll: Option<String>,
742    id:  Option<String>,
743    doc: Option<Value>,
744    caused_by: Option<Vec<serde_json::Value>>,
745}
746#[derive(Deserialize)]
747struct BatchBody { ops: Vec<BatchOp> }
748
749async fn batch_operations(
750    State(mgr): State<Manager>,
751    headers: HeaderMap,
752    AxPath(name): AxPath<String>,
753    Json(body): Json<BatchBody>,
754) -> Response {
755    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
756    let db = match mgr.get_db(&name).await {
757        None => match mgr.create_db(&name).await {
758            Ok(db) => db,
759            Err(e) => return err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
760        },
761        Some(db) => db,
762    };
763
764    if !db.startup_ready.load(std::sync::atomic::Ordering::SeqCst) {
765        return err(StatusCode::SERVICE_UNAVAILABLE,
766            "database startup in progress — reads available, writes retry in a moment");
767    }
768
769    // Split ops into puts (parallelisable) and deletes (sequential)
770    // Puts go through put_batch for parallel object + index writes.
771    // Deletes remain sequential (tombstone ordering matters).
772    let mut put_ops = vec![];
773    let mut del_ops: Vec<(String, String)> = vec![];
774    let mut op_order: Vec<(&str, usize)> = vec![];  // ("put"|"del", index into respective vec)
775
776    for op in &body.ops {
777        let t = op.op.to_lowercase();
778        match t.as_str() {
779            "put" => {
780                // Resolve caused_by items: accept hash strings (v2 native) OR seq integers (v1 compat).
781                let caused_by: Vec<String> = op.caused_by.clone().unwrap_or_default()
782                    .into_iter()
783                    .filter_map(|v| match v {
784                        serde_json::Value::String(s) => Some(s),
785                        serde_json::Value::Number(n) => {
786                            n.as_u64().and_then(|seq| db.get_hash_by_seq(seq))
787                        }
788                        _ => None,
789                    })
790                    .collect();
791                op_order.push(("put", put_ops.len()));
792                put_ops.push((
793                    op.coll.clone().unwrap_or_default(),
794                    op.id.clone().unwrap_or_default(),
795                    op.doc.clone().unwrap_or(json!({})),
796                    caused_by,
797                    None::<String>,
798                    None::<String>,
799                ));
800            }
801            "del" | "delete" => {
802                op_order.push(("del", del_ops.len()));
803                del_ops.push((
804                    op.coll.clone().unwrap_or_default(),
805                    op.id.clone().unwrap_or_default(),
806                ));
807            }
808            _ => { op_order.push(("unknown", 0)); }
809        }
810    }
811
812    // Execute all puts in parallel via put_batch
813    let put_results = if put_ops.is_empty() {
814        vec![]
815    } else {
816        match db.put_batch(put_ops) {
817            Ok(nodes) => nodes.into_iter().map(|n| json!({"op":"put","id":n.id,"seq":n.seq,"hash":n.hash})).collect(),
818            Err(e)    => return err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
819        }
820    };
821
822    // Execute deletes sequentially
823    let del_results: Vec<serde_json::Value> = del_ops.iter().map(|(coll, id)| {
824        match db.delete(coll, id) {
825            Ok(existed) => json!({"op":"del","id":id,"ok":existed}),
826            Err(e)      => json!({"op":"del","id":id,"error":e.to_string()}),
827        }
828    }).collect();
829
830    // Reconstruct results in original op order
831    let mut results = vec![];
832    for (kind, idx) in &op_order {
833        let r = match *kind {
834            "put"     => put_results.get(*idx).cloned().unwrap_or(json!({"op":"put","error":"missing"})),
835            "del"     => del_results.get(*idx).cloned().unwrap_or(json!({"op":"del","error":"missing"})),
836            _         => json!({"op": kind, "error": "unknown op"}),
837        };
838        results.push(r);
839    }
840    let (seq, head) = db_seq_head(&db);
841    // Notify live query subscribers after batch completes
842    mgr.notify_subscribers(&name, &db);
843    ok(json!({"results": results, "count": results.len(), "seq": seq, "head": head}))
844}
845
846#[derive(Deserialize)]
847struct IndexBody { coll: String, field: String, kind: Option<String> }
848
849async fn create_index(
850    State(mgr): State<Manager>,
851    headers: HeaderMap,
852    AxPath(name): AxPath<String>,
853    Json(body): Json<IndexBody>,
854) -> Response {
855    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
856    let db = match mgr.get_db(&name).await {
857        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
858        Some(db) => db,
859    };
860    let kind = body.kind.as_deref().unwrap_or("eq");
861    match kind {
862        "sorted" | "eq" => {
863            db.create_sorted_index(&body.coll, &body.field);
864            ok(json!({"ok": true, "coll": body.coll, "field": body.field, "kind": kind}))
865        }
866        _ => err(StatusCode::BAD_REQUEST, &format!("unknown index kind: {}", kind)),
867    }
868}
869
870async fn verify_database(
871    State(mgr): State<Manager>,
872    headers: HeaderMap,
873    AxPath(name): AxPath<String>,
874) -> Response {
875    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
876    let db = match mgr.get_db(&name).await {
877        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
878        Some(db) => db,
879    };
880    let (ok_count, tampered) = db.verify();
881    let (seq, head) = db_seq_head(&db);
882    ok(json!({
883        "ok": tampered.is_empty(),
884        "seq": seq,
885        "head": head,
886        "tamper_evident": true,
887        "objects_checked": ok_count,
888        "tampered": tampered,
889    }))
890}
891
892// ── State roots over HTTP ─────────────────────────────────────────────────
893//
894// The root surface is exposed because the thing a root is FOR is comparing two
895// databases, and the two databases are usually on two machines. A root you can
896// only compute locally answers a question nobody was asking.
897
898async fn root_current(
899    State(mgr): State<Manager>,
900    headers: HeaderMap,
901    AxPath(name): AxPath<String>,
902) -> Response {
903    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
904    let db = match mgr.get_db(&name).await {
905        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
906        Some(db) => db,
907    };
908    match db.state_root() {
909        Ok(r) => ok(serde_json::to_value(r).unwrap_or(json!({}))),
910        Err(e) => err(StatusCode::INTERNAL_SERVER_ERROR, &e),
911    }
912}
913
914async fn root_list(
915    State(mgr): State<Manager>,
916    headers: HeaderMap,
917    AxPath(name): AxPath<String>,
918) -> Response {
919    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
920    let db = match mgr.get_db(&name).await {
921        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
922        Some(db) => db,
923    };
924    ok(json!({
925        "roots": db.list_roots(),
926        "history_floor": db.history_floor(),
927    }))
928}
929
930async fn root_create(
931    State(mgr): State<Manager>,
932    headers: HeaderMap,
933    AxPath(name): AxPath<String>,
934    AxQuery(q): AxQuery<std::collections::HashMap<String, String>>,
935) -> Response {
936    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
937    let db = match mgr.get_db(&name).await {
938        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
939        Some(db) => db,
940    };
941    // `at` is explicit on purpose: a root at the tip is O(live state), a root
942    // in the past also walks a version chain per document. Backfill is not
943    // hidden behind the cheap call.
944    let at: Option<u64> = match q.get("at").map(|v| v.parse::<u64>()) {
945        None => None,
946        Some(Ok(v)) => Some(v),
947        Some(Err(_)) => return err(StatusCode::BAD_REQUEST, "at must be a sequence number"),
948    };
949    let made = match at {
950        Some(seq) => db.create_root_at(seq),
951        None => db.create_root(),
952    };
953    match made {
954        Ok(r) => ok(serde_json::to_value(r).unwrap_or(json!({}))),
955        Err(e) => err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
956    }
957}
958
959async fn root_verify(
960    State(mgr): State<Manager>,
961    headers: HeaderMap,
962    AxPath((name, seq)): AxPath<(String, u64)>,
963) -> Response {
964    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
965    let db = match mgr.get_db(&name).await {
966        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
967        Some(db) => db,
968    };
969    let v = db.verify_root(seq);
970    // Deliberately 200 for every outcome including a mismatch. The three states
971    // -- verified, mismatched, unverifiable -- are the PAYLOAD, and collapsing
972    // them onto HTTP status codes would re-flatten exactly the distinction the
973    // verification exists to preserve.
974    ok(json!({
975        "verified":   v.is_verified(),
976        "mismatch":   v.is_mismatch(),
977        "exit_code":  v.exit_code(),
978        "result":     v,
979    }))
980}
981
982async fn checkpoint(
983    State(mgr): State<Manager>,
984    headers: HeaderMap,
985    AxPath(name): AxPath<String>,
986) -> Response {
987    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
988    let db = match mgr.get_db(&name).await {
989        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
990        Some(db) => db,
991    };
992    let (seq, head) = db_seq_head(&db);
993    // v2 DAG is always "checkpointed" — content-addressed objects are inherently snapshotted
994    ok(json!({"ok": true, "head": head, "seq": seq}))
995}
996
997#[derive(Deserialize)]
998struct LogQuery { limit: Option<usize> }
999
1000async fn get_log(
1001    State(mgr): State<Manager>,
1002    headers: HeaderMap,
1003    AxPath(name): AxPath<String>,
1004    AxQuery(q): AxQuery<LogQuery>,
1005) -> Response {
1006    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
1007    let db = match mgr.get_db(&name).await {
1008        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
1009        Some(db) => db,
1010    };
1011    let limit = q.limit.unwrap_or(50);
1012    // v2: reconstruct log from objects (most recent first)
1013    let mut log_entries: Vec<Value> = db.objects.all_hashes()
1014        .filter_map(|h| db.objects.read(&h).ok())
1015        .take(limit)
1016        .map(|n| json!({
1017            "seq": n.seq, "coll": n.coll, "id": n.id,
1018            "hash": n.hash, "ts": n.ts, "op": "put"
1019        }))
1020        .collect();
1021    log_entries.sort_by(|a, b|
1022        b["seq"].as_u64().cmp(&a["seq"].as_u64())
1023    );
1024    log_entries.truncate(limit);
1025    let (seq, head) = db_seq_head(&db);
1026    ok(json!({"log": log_entries, "seq": seq, "head": head}))
1027}
1028
1029// ── tip / since — GET /v1/databases/:name/{tip,since} ─────────────────────────
1030// tip()   = the most recent write (head of the log), O(1).
1031// since() = the changefeed: every write after ?after_seq (exclusive), ascending.
1032// Both return full nodes (Node: Serialize), alongside the current seq + head.
1033
1034async fn tip_database(
1035    State(mgr): State<Manager>,
1036    headers: HeaderMap,
1037    AxPath(name): AxPath<String>,
1038) -> Response {
1039    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
1040    let db = match mgr.get_db(&name).await {
1041        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
1042        Some(db) => db,
1043    };
1044    let (seq, head) = db_seq_head(&db);
1045    let tip = db.tip().map(|n| serde_json::to_value(&n).unwrap_or(Value::Null));
1046    ok(json!({"tip": tip, "seq": seq, "head": head}))
1047}
1048
1049// Collection-local tip — GET /v1/databases/:name/collections/:coll/tip.
1050async fn tip_collection_database(
1051    State(mgr): State<Manager>,
1052    headers: HeaderMap,
1053    AxPath((name, coll)): AxPath<(String, String)>,
1054) -> Response {
1055    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
1056    let db = match mgr.get_db(&name).await {
1057        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
1058        Some(db) => db,
1059    };
1060    let (seq, head) = db_seq_head(&db);
1061    let tip = db.tip_collection(&coll).map(|n| serde_json::to_value(&n).unwrap_or(Value::Null));
1062    ok(json!({"coll": coll, "tip": tip, "seq": seq, "head": head}))
1063}
1064
1065#[derive(Deserialize)]
1066struct SinceQuery { after_seq: Option<u64>, limit: Option<usize> }
1067
1068async fn since_database(
1069    State(mgr): State<Manager>,
1070    headers: HeaderMap,
1071    AxPath(name): AxPath<String>,
1072    AxQuery(q): AxQuery<SinceQuery>,
1073) -> Response {
1074    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
1075    let db = match mgr.get_db(&name).await {
1076        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
1077        Some(db) => db,
1078    };
1079    let after = q.after_seq.unwrap_or(0);
1080    let b = db.since(after, q.limit.unwrap_or(0));
1081    let nodes: Vec<Value> = b.nodes.iter()
1082        .map(|n| serde_json::to_value(n).unwrap_or(Value::Null))
1083        .collect();
1084    let (seq, head) = db_seq_head(&db);
1085    ok(json!({
1086        "nodes": nodes, "count": nodes.len(),
1087        "from_seq": b.from_seq, "to_seq": b.to_seq, "head_seq": b.head_seq, "has_more": b.has_more,
1088        "seq": seq, "head": head
1089    }))
1090}
1091
1092// Replication readiness — GET /v1/databases/:name/status. scan_complete is the
1093// hard gate for correctness-critical catch-up (see Db::scan_status).
1094async fn status_database(
1095    State(mgr): State<Manager>,
1096    headers: HeaderMap,
1097    AxPath(name): AxPath<String>,
1098) -> Response {
1099    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
1100    let db = match mgr.get_db(&name).await {
1101        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
1102        Some(db) => db,
1103    };
1104    let s = db.scan_status();
1105    ok(json!({
1106        "ok": true,
1107        "scan_complete":   s.scan_complete,
1108        "tip_seq":         s.tip_seq,
1109        "indexed_seq_min": s.indexed_seq_min,
1110        "indexed_seq_max": s.indexed_seq_max,
1111        "indexed_count":   s.indexed_count
1112    }))
1113}
1114
1115// ── Live query subscriptions — POST /v1/databases/:name/subscribe ─────────────
1116
1117#[derive(Deserialize)]
1118struct SubscribeBody { nql: String }
1119
1120async fn subscribe_query(
1121    State(mgr): State<Manager>,
1122    headers: HeaderMap,
1123    AxPath(name): AxPath<String>,
1124    Json(body): Json<SubscribeBody>,
1125) -> Response {
1126    if !mgr.check_auth(&headers) {
1127        return err(StatusCode::UNAUTHORIZED, "unauthorized");
1128    }
1129    let db = match mgr.get_db(&name).await {
1130        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
1131        Some(db) => db,
1132    };
1133
1134    let (sub_id, rx) = mgr.subscribe(&name, body.nql.clone());
1135
1136    // Send the initial query result immediately as the first SSE event
1137    if let Ok((rows, _)) = crate::nql::query(&db, &body.nql) {
1138        let init = json!({
1139            "sub_id": sub_id,
1140            "db":     &name,
1141            "nql":    &body.nql,
1142            "rows":   rows,
1143            "count":  rows.len(),
1144            "event":  "initial",
1145        });
1146        // Update last_hash so we don't re-send this on the next write if unchanged
1147        if let Some(mut entry) = mgr.subs.get_mut(&(name.clone(), sub_id)) {
1148            let hash = format!("{:?}", rows);
1149            entry.value_mut().1 = hash;
1150        }
1151        // Send the initial result through the channel
1152        if let Some(entry) = mgr.subs.get(&(name.clone(), sub_id)) {
1153            let _ = entry.value().2.send(init.to_string());
1154        }
1155    }
1156
1157    let stream = BroadcastStream::new(rx).filter_map(|msg| {
1158        match msg {
1159            Ok(line) => Some(Ok::<Event, std::convert::Infallible>(Event::default().data(line))),
1160            Err(_)   => None,
1161        }
1162    });
1163    Sse::new(stream)
1164        .keep_alive(KeepAlive::default())
1165        .into_response()
1166}
1167
1168async fn unsubscribe_query(
1169    State(mgr): State<Manager>,
1170    headers: HeaderMap,
1171    AxPath((name, sub_id)): AxPath<(String, u64)>,
1172) -> Response {
1173    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
1174    mgr.unsubscribe(&name, sub_id);
1175    ok(json!({"ok": true, "sub_id": sub_id}))
1176}
1177
1178// ── SSE log stream — GET /events ──────────────────────────────────────────────
1179
1180async fn log_events(State(mgr): State<Manager>) -> Sse<impl futures_core::Stream<Item = Result<Event, std::convert::Infallible>>> {
1181    let rx = mgr.log_tx.subscribe();
1182    let stream = BroadcastStream::new(rx).filter_map(|msg| {
1183        match msg {
1184            Ok(line) => Some(Ok::<Event, std::convert::Infallible>(Event::default().data(line))),
1185            Err(_)   => None,  // lagged — skip
1186        }
1187    });
1188    Sse::new(stream).keep_alive(KeepAlive::default())
1189}
1190
1191// ── Router ────────────────────────────────────────────────────────────────────
1192
1193pub fn router(mgr: Manager) -> Router {
1194    Router::new()
1195        .route("/health",                                        get(health))
1196        .route("/events",                                        get(log_events))
1197        .route("/v1/databases",                                  get(list_databases).post(create_database))
1198        .route("/v1/databases/:name",                            get(get_database).delete(drop_database))
1199        .route("/v1/databases/:name/query",                      post(query_database))
1200        .route("/v1/databases/:name/cast",                       post(cast_prompt))
1201        .route("/v1/databases/:name/put",                        post(put_document))
1202        .route("/v1/databases/:name/link",                       post(link_document))
1203        // GET was missing here: a row could be DELETEd by id over HTTP but not
1204        // READ by id, forcing clients to interpolate the id into a NQL string.
1205        .route("/v1/databases/:name/rows/:coll/:id",
1206               get(get_document).delete(delete_document))
1207        .route("/v1/databases/:name/batch",                      post(batch_operations))
1208        .route("/v1/databases/:name/index",                      post(create_index))
1209        .route("/v1/databases/:name/verify",                     get(verify_database))
1210        .route("/v1/databases/:name/root",                       get(root_current).post(root_create))
1211        .route("/v1/databases/:name/roots",                      get(root_list))
1212        .route("/v1/databases/:name/roots/:seq/verify",          get(root_verify))
1213        .route("/v1/databases/:name/checkpoint",                 post(checkpoint))
1214        .route("/v1/databases/:name/log",                        get(get_log))
1215        .route("/v1/databases/:name/tip",                        get(tip_database))
1216        .route("/v1/databases/:name/collections/:coll/tip",      get(tip_collection_database))
1217        .route("/v1/databases/:name/since",                      get(since_database))
1218        .route("/v1/databases/:name/status",                     get(status_database))
1219        .route("/v1/databases/:name/subscribe",                  post(subscribe_query))
1220        .route("/v1/databases/:name/subscribe/:sub_id",          delete(unsubscribe_query))
1221        .with_state(mgr)
1222}
1223
1224/// Start the nedbd v2 server.
1225/// Lets the Postgres read endpoint share this process's already-open databases
1226/// instead of opening its own handles — which the exclusive data-dir LOCK would
1227/// refuse anyway, and rightly so.
1228impl crate::pgwire::DbResolver for Manager {
1229    fn resolve(&self, name: &str) -> Option<Arc<Db>> {
1230        // A blocking read on the manager map from the pgwire task. The lock is
1231        // only held across a HashMap lookup, never across I/O.
1232        let inner = self.inner.blocking_read();
1233        // An empty database name means the client did not send one; serve the
1234        // only database when that is unambiguous, which is the common case for
1235        // `psql -h host` against a single-database store.
1236        if name.is_empty() {
1237            if inner.dbs.len() == 1 {
1238                return inner.dbs.values().next().cloned();
1239            }
1240            return None;
1241        }
1242        inner.dbs.get(name).cloned()
1243    }
1244    fn token(&self) -> Option<String> {
1245        self.token.clone()
1246    }
1247}
1248
1249pub async fn run(host: &str, port: u16, data_dir: &str, tmk: Option<[u8; 32]>, token: Option<String>, memory_mode: bool) -> anyhow::Result<()> {
1250    // `mut` is required by the cast block below, which assigns mgr.caster. With
1251    // the feature off nothing mutates it, so an unconditional `mut` warns on
1252    // every default build — and warnings people are used to seeing are warnings
1253    // people stop reading.
1254    #[cfg(feature = "cast")]
1255    let mut mgr = Manager::new(Path::new(data_dir), tmk, token, memory_mode);
1256    #[cfg(not(feature = "cast"))]
1257    let mgr = Manager::new(Path::new(data_dir), tmk, token, memory_mode);
1258
1259    mgr.open_all().await?;
1260
1261    // Load the natural-language planner if this build has the feature AND the
1262    // operator asked for it. Failure to load is reported loudly but is NOT fatal:
1263    // a missing model should not stop a database from serving queries.
1264    #[cfg(feature = "cast")]
1265    {
1266        let want = std::env::var("NEDBD_CAST").map(|v| v == "1").unwrap_or(false);
1267        if want {
1268            match crate::cast::Caster::load(Path::new(data_dir)) {
1269                Ok(c) => {
1270                    println!("  cast     enabled — {:.2}M params, vocab {}, {}",
1271                             c.n_params() as f64 / 1e6, c.vocab_size(), c.source());
1272                    mgr.caster = Some(c);
1273                }
1274                Err(e) => {
1275                    eprintln!("  cast     DISABLED — {}", e);
1276                }
1277            }
1278        }
1279    }
1280    // Freeze it: nothing past this point should mutate the manager. Only
1281    // meaningful in the cast build, where `mgr` was declared `mut` above.
1282    #[cfg(feature = "cast")]
1283    let mgr = mgr;
1284
1285    let has_token = mgr.token.is_some();
1286    let mgr_for_shutdown = mgr.clone();
1287    // ── Postgres read endpoint ────────────────────────────────────────────────
1288    // Opt-in: nothing binds unless NEDBD_PG_PORT is set (or --pg-port passed).
1289    // Default-off is deliberate — a second listener is a second attack surface,
1290    // and it speaks cleartext, so the operator asks for it explicitly.
1291    if let Ok(raw) = std::env::var("NEDBD_PG_PORT") {
1292        match raw.trim().parse::<u16>() {
1293            Ok(pg_port) if pg_port > 0 => {
1294                let pg_host = host.to_string();
1295                let resolver: Arc<dyn crate::pgwire::DbResolver> = Arc::new(mgr.clone());
1296                tokio::spawn(async move {
1297                    if let Err(e) = crate::pgwire::run(&pg_host, pg_port, resolver).await {
1298                        eprintln!("  [pgwire] listener stopped: {}", e);
1299                    }
1300                });
1301            }
1302            _ => eprintln!("  [pgwire] ignoring NEDBD_PG_PORT={:?} — not a valid port", raw),
1303        }
1304    }
1305
1306    let app = router(mgr);
1307    let addr = format!("{}:{}", host, port).parse::<std::net::SocketAddr>()?;
1308    let banner = format!(r#"
13091310          ╱ ╲               N E D B  ·  DAG ENGINE  {}
1311         ◆   ◆              ─────────────────────────────────────────────
1312        ╱ ╲ ╱ ╲             content-addressed · tamper-evident · causal
1313       ◆   ◆   ◆            bi-temporal · replay-protected · encrypted
1314      ╱ ╲ ╱ ╲ ╱ ╲
1315     ◆   ◆   ◆   ◆          © INTERCHAINED LLC × Vex (Interchained AI fleet: GLM · Claude · Opus · Fable · GPT-6)
1316    ╱ ╲ ╱ ╲ ╱ ╲ ╱ ╲         interchained.org   ·   hyperagent.com/refer/J2G6TCD7
1317
1318  ─────────────────────────────────────────────────────────────
1319  listen   http://{}
1320  data     {}
1321  enc      {}
1322  token    {}
1323  memory   {}
1324  ─────────────────────────────────────────────────────────────
1325"#,
1326        env!("CARGO_PKG_VERSION"),
1327        addr,
1328        data_dir,
1329        if tmk.is_some() { "AES-256-GCM" } else { "off" },
1330        if has_token { "on" } else { "off (set NEDBD_TOKEN to require auth)" },
1331        if memory_mode { "yes — all data lost on exit (NEDBD_MEMORY=1)" } else { "no — durable DAG on disk" }
1332    );
1333    print!("{}", banner);
1334
1335    let listener = tokio::net::TcpListener::bind(addr).await?;
1336
1337    // ── Scheduled hourly checkpoint ────────────────────────────────────────────
1338    // Flush MANIFEST every hour aligned to the system clock (top of the hour).
1339    // Ensures warm-start data is always fresh even on long-running servers.
1340    let mgr_hourly = mgr_for_shutdown.clone();
1341    tokio::spawn(async move {
1342        loop {
1343            // Sleep until the next top-of-hour boundary
1344            let now_secs = std::time::SystemTime::now()
1345                .duration_since(std::time::UNIX_EPOCH)
1346                .map(|d| d.as_secs()).unwrap_or(0);
1347            let secs_into_hour = now_secs % 3600;
1348            let sleep_secs = 3600 - secs_into_hour;
1349            tokio::time::sleep(tokio::time::Duration::from_secs(sleep_secs)).await;
1350            mgr_hourly.flush_all().await;
1351            println!("  [nedbd] hourly checkpoint — manifests flushed");
1352        }
1353    });
1354
1355    // ── Graceful shutdown: SIGINT (Ctrl+C) + SIGTERM (systemctl stop) ─────────
1356    let shutdown = async {
1357        #[cfg(unix)]
1358        {
1359            use tokio::signal::unix::{signal, SignalKind};
1360            let mut sigterm = signal(SignalKind::terminate()).unwrap();
1361            let mut sigint  = signal(SignalKind::interrupt()).unwrap();
1362            tokio::select! {
1363                _ = sigterm.recv() => println!("  [nedbd] SIGTERM — flushing and exiting..."),
1364                _ = sigint.recv()  => println!("  [nedbd] SIGINT  — flushing and exiting..."),
1365            }
1366        }
1367        #[cfg(not(unix))]
1368        {
1369            tokio::signal::ctrl_c().await.ok();
1370            println!("  [nedbd] shutting down — flushing manifests...");
1371        }
1372    };
1373
1374    axum::serve(listener, app)
1375        .tcp_nodelay(true)
1376        .with_graceful_shutdown(shutdown)
1377        .await?;
1378
1379    // Final flush on exit
1380    mgr_for_shutdown.flush_all().await;
1381    println!("  [nedbd] goodbye");
1382    Ok(())
1383}