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